dawarich/e2e/live-mode.spec.js
2025-08-01 17:03:05 +02:00

1216 lines
44 KiB
JavaScript

import { test, expect } from '@playwright/test';
/**
* These tests cover the Live Mode functionality of the /map page
* Live Mode allows real-time streaming of GPS points via WebSocket
*/
test.describe('Live Mode Functionality', () => {
let page;
let context;
test.beforeAll(async ({ browser }) => {
context = await browser.newContext();
page = await context.newPage();
// Sign in once for all tests
await page.goto('/users/sign_in');
await page.waitForSelector('input[name="user[email]"]', { timeout: 10000 });
await page.fill('input[name="user[email]"]', 'demo@dawarich.app');
await page.fill('input[name="user[password]"]', 'password');
await page.click('input[type="submit"][value="Log in"]');
// Wait for redirect to map page
await page.waitForURL('/map', { timeout: 10000 });
await page.waitForSelector('#map', { timeout: 10000 });
await page.waitForSelector('.leaflet-container', { timeout: 10000 });
});
test.afterAll(async () => {
await page.close();
await context.close();
});
test.beforeEach(async () => {
// Navigate to June 4, 2025 where we have test data
await page.goto('/map?start_at=2025-06-04T00:00&end_at=2025-06-04T23:59');
await page.waitForSelector('#map', { timeout: 10000 });
await page.waitForSelector('.leaflet-container', { timeout: 10000 });
// Wait for map controller to be initialized
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
// Give controllers time to connect (best effort)
await page.waitForTimeout(3000);
});
test.describe('Live Mode Debug', () => {
test('should debug current map state and point processing', async () => {
// Don't enable live mode initially - check base state
console.log('=== DEBUGGING MAP STATE ===');
// Check initial state
const initialState = await page.evaluate(() => {
const mapElement = document.querySelector('#map');
// Check various ways to find the controller
const stimulusControllers = mapElement?._stimulus_controllers;
const mapController = stimulusControllers?.find(c => c.identifier === 'maps');
// Check if Stimulus is loaded at all
const hasStimulus = !!(window.Stimulus || window.Application);
// Check data attributes
const hasDataController = mapElement?.hasAttribute('data-controller');
const dataControllerValue = mapElement?.getAttribute('data-controller');
return {
// Map element data
hasMapElement: !!mapElement,
hasApiKey: !!mapElement?.dataset.api_key,
hasCoordinates: !!mapElement?.dataset.coordinates,
hasUserSettings: !!mapElement?.dataset.user_settings,
// Stimulus debugging
hasStimulus: hasStimulus,
hasDataController: hasDataController,
dataControllerValue: dataControllerValue,
hasStimulusControllers: !!stimulusControllers,
stimulusControllersCount: stimulusControllers?.length || 0,
controllerIdentifiers: stimulusControllers?.map(c => c.identifier) || [],
// Map controller
hasMapController: !!mapController,
controllerProps: mapController ? Object.keys(mapController) : [],
// Live mode specific
liveMapEnabled: mapController?.liveMapEnabled,
// Markers and data
markersLength: mapController?.markers?.length || 0,
markersArrayLength: mapController?.markersArray?.length || 0,
// WebSocket
hasConsumer: !!(window.App?.cable || window.consumer),
// Date range from URL
currentUrl: window.location.href
};
});
console.log('Initial state:', JSON.stringify(initialState, null, 2));
// Check DOM elements
const domCounts = await page.evaluate(() => ({
markerElements: document.querySelectorAll('.leaflet-marker-pane .leaflet-marker-icon').length,
polylineElements: document.querySelectorAll('.leaflet-overlay-pane path').length,
totalLeafletElements: document.querySelectorAll('[class*="leaflet"]').length
}));
console.log('DOM counts:', domCounts);
// Now enable live mode and check again
await enableLiveMode(page);
const afterLiveModeState = await page.evaluate(() => {
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
return {
liveMapEnabled: mapController?.liveMapEnabled,
markersLength: mapController?.markers?.length || 0,
hasAppendPointMethod: typeof mapController?.appendPoint === 'function'
};
});
console.log('After enabling live mode:', afterLiveModeState);
// Try direct Leaflet map manipulation to trigger memory leak
console.log('Testing direct Leaflet map manipulation...');
const directResult = await page.evaluate(() => {
// Try multiple ways to find the Leaflet map instance
const mapContainer = document.querySelector('#map [data-maps-target="container"]');
// Debug info
const debugInfo = {
hasMapContainer: !!mapContainer,
hasLeafletId: mapContainer?._leaflet_id,
leafletId: mapContainer?._leaflet_id,
hasL: typeof L !== 'undefined',
windowKeys: Object.keys(window).filter(k => k.includes('L_')).slice(0, 5)
};
if (!mapContainer) {
return { success: false, error: 'No map container found', debug: debugInfo };
}
// Try different ways to get the map
let map = null;
// Method 1: Direct reference
if (mapContainer._leaflet_id) {
map = window[`L_${mapContainer._leaflet_id}`] || mapContainer._leaflet_map;
}
// Method 2: Check if container has map directly
if (!map && mapContainer._leaflet_map) {
map = mapContainer._leaflet_map;
}
// Method 3: Check Leaflet's internal registry
if (!map && typeof L !== 'undefined' && L.Util && L.Util.stamp && mapContainer._leaflet_id) {
// Try to find in Leaflet's internal map registry
if (window.L && window.L._map) {
map = window.L._map;
}
}
// Method 4: Try to find any existing map instance in the DOM
if (!map) {
const leafletContainers = document.querySelectorAll('.leaflet-container');
for (let container of leafletContainers) {
if (container._leaflet_map) {
map = container._leaflet_map;
break;
}
}
}
if (map && typeof L !== 'undefined') {
try {
// Create a simple marker to test if the map works
const testMarker = L.marker([52.52, 13.40], {
icon: L.divIcon({
className: 'test-marker',
html: '<div style="background: red; width: 10px; height: 10px; border-radius: 50%;"></div>',
iconSize: [10, 10]
})
});
// Add directly to map
testMarker.addTo(map);
return {
success: true,
error: null,
markersAdded: 1,
debug: debugInfo
};
} catch (error) {
return { success: false, error: error.message, debug: debugInfo };
}
}
return { success: false, error: 'No usable Leaflet map found', debug: debugInfo };
});
// Check after direct manipulation
const afterDirectCall = await page.evaluate(() => {
return {
domMarkers: document.querySelectorAll('.leaflet-marker-pane .leaflet-marker-icon').length,
domLayerGroups: document.querySelectorAll('.leaflet-layer').length,
totalLeafletElements: document.querySelectorAll('[class*="leaflet"]').length
};
});
console.log('Direct manipulation result:', directResult);
console.log('After direct manipulation:', afterDirectCall);
// Try WebSocket simulation
console.log('Testing WebSocket simulation...');
const wsResult = await simulateWebSocketMessage(page, {
lat: 52.521008,
lng: 13.405954,
timestamp: new Date('2025-06-04T12:01:00').getTime(),
id: Date.now() + 1
});
console.log('WebSocket result:', wsResult);
// Final check
const finalState = await page.evaluate(() => {
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
return {
markersLength: mapController?.markers?.length || 0,
markersArrayLength: mapController?.markersArray?.length || 0,
domMarkers: document.querySelectorAll('.leaflet-marker-pane .leaflet-marker-icon').length,
domPolylines: document.querySelectorAll('.leaflet-overlay-pane path').length
};
});
console.log('Final state:', finalState);
console.log('=== END DEBUGGING ===');
// This test is just for debugging, so always pass
expect(true).toBe(true);
});
});
test.describe('Live Mode Settings', () => {
test('should have live mode checkbox in settings panel', async () => {
// Open settings panel
await page.waitForSelector('.map-settings-button', { timeout: 10000 });
const settingsButton = page.locator('.map-settings-button');
await settingsButton.click();
await page.waitForTimeout(500);
// Verify live mode checkbox exists
const liveMapCheckbox = page.locator('#live_map_enabled');
await expect(liveMapCheckbox).toBeVisible();
// Verify checkbox has proper attributes
await expect(liveMapCheckbox).toHaveAttribute('type', 'checkbox');
await expect(liveMapCheckbox).toHaveAttribute('name', 'live_map_enabled');
// Verify checkbox label exists
const liveMapLabel = page.locator('label[for="live_map_enabled"]');
await expect(liveMapLabel).toBeVisible();
// Close settings panel
await settingsButton.click();
await page.waitForTimeout(500);
});
test('should enable and disable live mode via settings', async () => {
// Open settings panel
const settingsButton = page.locator('.map-settings-button');
await settingsButton.click();
await page.waitForTimeout(500);
const liveMapCheckbox = page.locator('#live_map_enabled');
const submitButton = page.locator('#settings-form button[type="submit"]');
// Ensure elements are visible
await expect(liveMapCheckbox).toBeVisible();
await expect(submitButton).toBeVisible();
// Get initial state
const initiallyChecked = await liveMapCheckbox.isChecked();
// Toggle live mode
if (initiallyChecked) {
await liveMapCheckbox.uncheck();
} else {
await liveMapCheckbox.check();
}
// Verify checkbox state changed
const newState = await liveMapCheckbox.isChecked();
expect(newState).toBe(!initiallyChecked);
// Submit the form
await submitButton.click();
await page.waitForTimeout(3000); // Longer wait for form submission
// Check if panel closed after submission or stayed open
const panelStillVisible = await page.locator('.leaflet-settings-panel').isVisible().catch(() => false);
if (panelStillVisible) {
// Panel stayed open - verify the checkbox state directly
const persistedCheckbox = page.locator('#live_map_enabled');
await expect(persistedCheckbox).toBeVisible();
const persistedState = await persistedCheckbox.isChecked();
expect(persistedState).toBe(newState);
// Reset to original state for cleanup
if (persistedState !== initiallyChecked) {
await persistedCheckbox.click();
await submitButton.click();
await page.waitForTimeout(2000);
}
// Close settings panel
await settingsButton.click();
await page.waitForTimeout(500);
} else {
// Panel closed - reopen to verify persistence
await settingsButton.click();
await page.waitForTimeout(1000);
const persistedCheckbox = page.locator('#live_map_enabled');
await expect(persistedCheckbox).toBeVisible();
// Verify the setting was persisted
const persistedState = await persistedCheckbox.isChecked();
expect(persistedState).toBe(newState);
// Reset to original state for cleanup
if (persistedState !== initiallyChecked) {
await persistedCheckbox.click();
const resetSubmitButton = page.locator('#settings-form button[type="submit"]');
await resetSubmitButton.click();
await page.waitForTimeout(2000);
}
// Close settings panel
await settingsButton.click();
await page.waitForTimeout(500);
}
});
});
test.describe('WebSocket Connection Management', () => {
test('should establish WebSocket connection when live mode is enabled', async () => {
// Enable live mode first
await enableLiveMode(page);
// Monitor WebSocket connections
const wsConnections = [];
page.on('websocket', ws => {
console.log(`WebSocket connection: ${ws.url()}`);
wsConnections.push(ws);
});
// Reload page to trigger WebSocket connection with live mode enabled
await page.reload();
await page.waitForSelector('.leaflet-container', { timeout: 10000 });
await page.waitForTimeout(3000); // Wait for WebSocket connection
// Verify WebSocket connection was established
// Note: This might not work in all test environments, so we'll also check for JavaScript evidence
const hasWebSocketConnection = await page.evaluate(() => {
// Check if ActionCable consumer exists and has subscriptions
return window.App && window.App.cable && window.App.cable.subscriptions;
});
if (hasWebSocketConnection) {
console.log('WebSocket connection established via ActionCable');
} else {
// Alternative check: look for PointsChannel subscription in the DOM/JavaScript
const hasPointsChannelSubscription = await page.evaluate(() => {
// Check for evidence of PointsChannel subscription
return document.querySelector('[data-controller*="maps"]') !== null;
});
expect(hasPointsChannelSubscription).toBe(true);
}
});
test('should handle WebSocket connection errors gracefully', async () => {
// Enable live mode
await enableLiveMode(page);
// Monitor console errors
const consoleErrors = [];
page.on('console', message => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
// Verify initial state - map should be working
await expect(page.locator('.leaflet-container')).toBeVisible();
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
// Test connection resilience by simulating various network conditions
try {
// Simulate brief network interruption
await page.context().setOffline(true);
await page.waitForTimeout(1000); // Brief disconnection
// Restore network
await page.context().setOffline(false);
await page.waitForTimeout(2000); // Wait for reconnection
// Verify map still functions after network interruption
await expect(page.locator('.leaflet-container')).toBeVisible();
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
// Test basic map interactions still work
const layerControl = page.locator('.leaflet-control-layers');
await layerControl.click();
// Wait for layer control to open, with fallback
try {
await expect(page.locator('.leaflet-control-layers-list')).toBeVisible({ timeout: 3000 });
} catch (e) {
// Layer control might not expand in test environment, just check it's clickable
console.log('Layer control may not expand in test environment');
}
// Verify settings panel still works
const settingsButton = page.locator('.map-settings-button');
await settingsButton.click();
await page.waitForTimeout(500);
await expect(page.locator('.leaflet-settings-panel')).toBeVisible();
// Close settings panel
await settingsButton.click();
await page.waitForTimeout(500);
} catch (error) {
console.log('Network simulation error (expected in some test environments):', error.message);
// Even if network simulation fails, verify basic functionality
await expect(page.locator('.leaflet-container')).toBeVisible();
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
}
// WebSocket errors might occur but shouldn't break the application
const applicationRemainsStable = await page.locator('.leaflet-container').isVisible();
expect(applicationRemainsStable).toBe(true);
console.log(`Console errors detected during connection test: ${consoleErrors.length}`);
});
});
test.describe('Point Streaming and Memory Management', () => {
test('should handle single point addition without memory leaks', async () => {
// Enable live mode
await enableLiveMode(page);
// Get initial memory baseline
const initialMemory = await getMemoryUsage(page);
// Get initial marker count
const initialMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
// Simulate a single point being received via WebSocket
// Using coordinates from June 4, 2025 test data range
await simulatePointReceived(page, {
lat: 52.520008, // Berlin coordinates (matching existing test data)
lng: 13.404954,
timestamp: new Date('2025-06-04T12:00:00').getTime(),
id: Date.now()
});
await page.waitForTimeout(1000); // Wait for point processing
// Verify point was added to map
const newMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
expect(newMarkerCount).toBeGreaterThanOrEqual(initialMarkerCount);
// Check memory usage hasn't increased dramatically
const finalMemory = await getMemoryUsage(page);
const memoryIncrease = finalMemory.usedJSHeapSize - initialMemory.usedJSHeapSize;
// Allow for reasonable memory increase (less than 50MB for a single point)
expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024);
console.log(`Memory increase for single point: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
});
test('should handle multiple point additions without exponential memory growth', async () => {
// Enable live mode
await enableLiveMode(page);
// Get initial memory baseline
const initialMemory = await getMemoryUsage(page);
const memoryMeasurements = [initialMemory.usedJSHeapSize];
// Simulate multiple points being received
const pointCount = 10;
const baseTimestamp = new Date('2025-06-04T12:00:00').getTime();
for (let i = 0; i < pointCount; i++) {
await simulatePointReceived(page, {
lat: 52.520008 + (i * 0.001), // Slightly different positions around Berlin
lng: 13.404954 + (i * 0.001),
timestamp: baseTimestamp + (i * 60000), // 1 minute intervals
id: baseTimestamp + i
});
await page.waitForTimeout(200); // Small delay between points
// Measure memory every few points
if ((i + 1) % 3 === 0) {
const currentMemory = await getMemoryUsage(page);
memoryMeasurements.push(currentMemory.usedJSHeapSize);
}
}
// Final memory measurement
const finalMemory = await getMemoryUsage(page);
memoryMeasurements.push(finalMemory.usedJSHeapSize);
// Analyze memory growth pattern
const totalMemoryIncrease = finalMemory.usedJSHeapSize - initialMemory.usedJSHeapSize;
const averageIncreasePerPoint = totalMemoryIncrease / pointCount;
console.log(`Total memory increase for ${pointCount} points: ${(totalMemoryIncrease / 1024 / 1024).toFixed(2)}MB`);
console.log(`Average memory per point: ${(averageIncreasePerPoint / 1024 / 1024).toFixed(2)}MB`);
// Memory increase should be reasonable (less than 10MB per point)
expect(averageIncreasePerPoint).toBeLessThan(10 * 1024 * 1024);
// Check for exponential growth by comparing early vs late increases
if (memoryMeasurements.length >= 3) {
const earlyIncrease = memoryMeasurements[1] - memoryMeasurements[0];
const lateIncrease = memoryMeasurements[memoryMeasurements.length - 1] - memoryMeasurements[memoryMeasurements.length - 2];
const growthRatio = lateIncrease / Math.max(earlyIncrease, 1024 * 1024); // Avoid division by zero
// Growth ratio should not be exponential (less than 10x increase)
expect(growthRatio).toBeLessThan(10);
console.log(`Memory growth ratio (late/early): ${growthRatio.toFixed(2)}`);
}
});
test('should properly cleanup layers during continuous point streaming', async () => {
// Enable live mode
await enableLiveMode(page);
// Count initial DOM nodes
const initialNodeCount = await page.evaluate(() => {
return document.querySelectorAll('.leaflet-marker-pane *, .leaflet-overlay-pane *').length;
});
// Simulate rapid point streaming
const streamPoints = async (count) => {
const baseTimestamp = new Date('2025-06-04T12:00:00').getTime();
for (let i = 0; i < count; i++) {
await simulatePointReceived(page, {
lat: 52.520008 + (Math.random() * 0.01), // Random positions around Berlin
lng: 13.404954 + (Math.random() * 0.01),
timestamp: baseTimestamp + (i * 10000), // 10 second intervals for rapid streaming
id: baseTimestamp + i
});
// Very small delay to simulate rapid streaming
await page.waitForTimeout(50);
}
};
// Stream first batch
await streamPoints(5);
await page.waitForTimeout(1000);
const midNodeCount = await page.evaluate(() => {
return document.querySelectorAll('.leaflet-marker-pane *, .leaflet-overlay-pane *').length;
});
// Stream second batch
await streamPoints(5);
await page.waitForTimeout(1000);
const finalNodeCount = await page.evaluate(() => {
return document.querySelectorAll('.leaflet-marker-pane *, .leaflet-overlay-pane *').length;
});
console.log(`DOM nodes - Initial: ${initialNodeCount}, Mid: ${midNodeCount}, Final: ${finalNodeCount}`);
// DOM nodes should not grow unbounded
// Allow for some growth but not exponential
const nodeGrowthRatio = finalNodeCount / Math.max(initialNodeCount, 1);
expect(nodeGrowthRatio).toBeLessThan(50); // Should not be more than 50x initial nodes
// Verify layers are being managed properly
const layerElements = await page.evaluate(() => {
const markers = document.querySelectorAll('.leaflet-marker-pane .leaflet-marker-icon');
const polylines = document.querySelectorAll('.leaflet-overlay-pane path');
return {
markerCount: markers.length,
polylineCount: polylines.length
};
});
console.log(`Final counts - Markers: ${layerElements.markerCount}, Polylines: ${layerElements.polylineCount}`);
// Verify we have reasonable number of elements (not accumulating infinitely)
expect(layerElements.markerCount).toBeLessThan(1000);
expect(layerElements.polylineCount).toBeLessThan(1000);
});
test('should handle map view updates during point streaming', async () => {
// Enable live mode
await enableLiveMode(page);
// Get initial map center
const initialCenter = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
if (container && container._leaflet_id) {
const map = window[`L_${container._leaflet_id}`];
if (map) {
const center = map.getCenter();
return { lat: center.lat, lng: center.lng };
}
}
return null;
});
// Simulate point at different location (but within reasonable test data range)
const newPointLocation = {
lat: 52.5200, // Slightly different Berlin location
lng: 13.4050,
timestamp: new Date('2025-06-04T14:00:00').getTime(),
id: Date.now()
};
await simulatePointReceived(page, newPointLocation);
await page.waitForTimeout(2000); // Wait for map to potentially update
// Verify map view was updated to new location
const newCenter = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
if (container && container._leaflet_id) {
const map = window[`L_${container._leaflet_id}`];
if (map) {
const center = map.getCenter();
return { lat: center.lat, lng: center.lng };
}
}
return null;
});
if (initialCenter && newCenter) {
// Map should have moved to the new point location
const latDifference = Math.abs(newCenter.lat - newPointLocation.lat);
const lngDifference = Math.abs(newCenter.lng - newPointLocation.lng);
// Should be close to the new point (within reasonable tolerance)
expect(latDifference).toBeLessThan(0.1);
expect(lngDifference).toBeLessThan(0.1);
console.log(`Map moved from [${initialCenter.lat}, ${initialCenter.lng}] to [${newCenter.lat}, ${newCenter.lng}]`);
}
});
test('should handle realistic WebSocket message streaming', async () => {
// Enable live mode
await enableLiveMode(page);
// Debug: Check if live mode is actually enabled
const liveMode = await page.evaluate(() => {
const mapElement = document.querySelector('#map');
const userSettings = mapElement?.dataset.user_settings;
if (userSettings) {
try {
const settings = JSON.parse(userSettings);
return settings.live_map_enabled;
} catch (e) {
return 'parse_error';
}
}
return 'no_settings';
});
console.log('Live mode enabled:', liveMode);
// Debug: Check WebSocket connection
const wsStatus = await page.evaluate(() => {
const consumer = window.App?.cable || window.consumer;
if (consumer && consumer.subscriptions) {
const pointsSubscription = consumer.subscriptions.subscriptions.find(sub =>
sub.identifier && JSON.parse(sub.identifier).channel === 'PointsChannel'
);
return {
hasConsumer: !!consumer,
hasSubscriptions: !!consumer.subscriptions,
subscriptionCount: consumer.subscriptions.subscriptions?.length || 0,
hasPointsChannel: !!pointsSubscription
};
}
return { hasConsumer: false, error: 'no_consumer' };
});
console.log('WebSocket status:', wsStatus);
// Get initial memory and marker count
const initialMemory = await getMemoryUsage(page);
const initialMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
console.log('Testing realistic WebSocket message simulation...');
console.log('Initial markers:', initialMarkerCount);
// Use the more realistic WebSocket simulation
const pointCount = 15;
const baseTimestamp = new Date('2025-06-04T12:00:00').getTime();
for (let i = 0; i < pointCount; i++) {
await simulateWebSocketMessage(page, {
lat: 52.520008 + (i * 0.0005), // Gradual movement
lng: 13.404954 + (i * 0.0005),
timestamp: baseTimestamp + (i * 30000), // 30 second intervals
id: baseTimestamp + i
});
// Realistic delay between points
await page.waitForTimeout(100);
// Monitor memory every 5 points
if ((i + 1) % 5 === 0) {
const currentMemory = await getMemoryUsage(page);
const memoryIncrease = currentMemory.usedJSHeapSize - initialMemory.usedJSHeapSize;
console.log(`After ${i + 1} points: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB increase`);
}
}
// Final measurements
const finalMemory = await getMemoryUsage(page);
const finalMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
const totalMemoryIncrease = finalMemory.usedJSHeapSize - initialMemory.usedJSHeapSize;
const averageMemoryPerPoint = totalMemoryIncrease / pointCount;
console.log(`WebSocket simulation - Total memory increase: ${(totalMemoryIncrease / 1024 / 1024).toFixed(2)}MB`);
console.log(`Average memory per point: ${(averageMemoryPerPoint / 1024 / 1024).toFixed(2)}MB`);
console.log(`Markers: ${initialMarkerCount}${finalMarkerCount}`);
// Debug: Check what's in the map data
const mapDebugInfo = await page.evaluate(() => {
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
if (mapController) {
return {
hasMarkers: !!mapController.markers,
markersLength: mapController.markers?.length || 0,
hasMarkersArray: !!mapController.markersArray,
markersArrayLength: mapController.markersArray?.length || 0,
liveMapEnabled: mapController.liveMapEnabled
};
}
return { error: 'No map controller found' };
});
console.log('Map controller debug:', mapDebugInfo);
// Verify reasonable memory usage (allow more for realistic simulation)
expect(averageMemoryPerPoint).toBeLessThan(20 * 1024 * 1024); // 20MB per point max
expect(finalMarkerCount).toBeGreaterThanOrEqual(initialMarkerCount);
});
test('should handle continuous realistic streaming with variable timing', async () => {
// Enable live mode
await enableLiveMode(page);
// Get initial state
const initialMemory = await getMemoryUsage(page);
const initialDOMNodes = await page.evaluate(() => {
return document.querySelectorAll('.leaflet-marker-pane *, .leaflet-overlay-pane *').length;
});
console.log('Testing continuous realistic streaming...');
// Use the realistic streaming function
await simulateRealtimeStream(page, {
pointCount: 12,
maxInterval: 500, // Faster for testing
minInterval: 50,
driftRange: 0.002 // More realistic GPS drift
});
// Let the system settle
await page.waitForTimeout(1000);
// Final measurements
const finalMemory = await getMemoryUsage(page);
const finalDOMNodes = await page.evaluate(() => {
return document.querySelectorAll('.leaflet-marker-pane *, .leaflet-overlay-pane *').length;
});
const memoryIncrease = finalMemory.usedJSHeapSize - initialMemory.usedJSHeapSize;
const domNodeIncrease = finalDOMNodes - initialDOMNodes;
console.log(`Realistic streaming - Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB`);
console.log(`DOM nodes: ${initialDOMNodes}${finalDOMNodes} (${domNodeIncrease} increase)`);
// Verify system stability
await expect(page.locator('.leaflet-container')).toBeVisible();
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
// Memory should be reasonable for realistic streaming
expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024); // 100MB max for 12 points
// DOM nodes shouldn't grow unbounded
expect(domNodeIncrease).toBeLessThan(500);
});
});
test.describe('Live Mode Error Handling', () => {
test('should handle malformed point data gracefully', async () => {
// Enable live mode
await enableLiveMode(page);
// Monitor console errors
const consoleErrors = [];
page.on('console', message => {
if (message.type() === 'error') {
consoleErrors.push(message.text());
}
});
// Get initial marker count
const initialMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
// Simulate malformed point data
await page.evaluate(() => {
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
if (mapController && mapController.appendPoint) {
// Try various malformed data scenarios
try {
mapController.appendPoint(null);
} catch (e) {
console.log('Handled null data');
}
try {
mapController.appendPoint({});
} catch (e) {
console.log('Handled empty object');
}
try {
mapController.appendPoint([]);
} catch (e) {
console.log('Handled empty array');
}
try {
mapController.appendPoint(['invalid', 'data']);
} catch (e) {
console.log('Handled invalid array data');
}
}
});
await page.waitForTimeout(1000);
// Verify map is still functional
await expect(page.locator('.leaflet-container')).toBeVisible();
// Marker count should not have changed (malformed data should be rejected)
const finalMarkerCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
expect(finalMarkerCount).toBe(initialMarkerCount);
// Some errors are expected from malformed data, but application should continue working
const layerControlWorks = await page.locator('.leaflet-control-layers').isVisible();
expect(layerControlWorks).toBe(true);
});
test('should recover from JavaScript errors during point processing', async () => {
// Enable live mode
await enableLiveMode(page);
// Inject a temporary error into the point processing
await page.evaluate(() => {
// Temporarily break a method to simulate an error
const originalCreateMarkersArray = window.createMarkersArray;
let errorInjected = false;
// Override function temporarily to cause an error once
if (window.createMarkersArray) {
window.createMarkersArray = function(...args) {
if (!errorInjected) {
errorInjected = true;
throw new Error('Simulated processing error');
}
return originalCreateMarkersArray.apply(this, args);
};
// Restore original function after a delay
setTimeout(() => {
window.createMarkersArray = originalCreateMarkersArray;
}, 2000);
}
});
// Try to add a point (should trigger error first time)
await simulatePointReceived(page, {
lat: 52.520008,
lng: 13.404954,
timestamp: new Date('2025-06-04T13:00:00').getTime(),
id: Date.now()
});
await page.waitForTimeout(1000);
// Verify map is still responsive
await expect(page.locator('.leaflet-container')).toBeVisible();
// Try adding another point (should work after recovery)
await page.waitForTimeout(2000); // Wait for function restoration
await simulatePointReceived(page, {
lat: 52.521008,
lng: 13.405954,
timestamp: new Date('2025-06-04T13:30:00').getTime(),
id: Date.now() + 1000
});
await page.waitForTimeout(1000);
// Verify map functionality has recovered
const layerControl = page.locator('.leaflet-control-layers');
await expect(layerControl).toBeVisible();
await layerControl.click();
await expect(page.locator('.leaflet-control-layers-list')).toBeVisible();
});
});
});
// Helper functions
/**
* Enable live mode via settings panel
*/
async function enableLiveMode(page) {
const settingsButton = page.locator('.map-settings-button');
await settingsButton.click();
await page.waitForTimeout(500);
// Ensure settings panel is open
await expect(page.locator('.leaflet-settings-panel')).toBeVisible();
const liveMapCheckbox = page.locator('#live_map_enabled');
await expect(liveMapCheckbox).toBeVisible();
const isEnabled = await liveMapCheckbox.isChecked();
if (!isEnabled) {
await liveMapCheckbox.check();
const submitButton = page.locator('#settings-form button[type="submit"]');
await expect(submitButton).toBeVisible();
await submitButton.click();
await page.waitForTimeout(3000); // Longer wait for settings to save
// Check if panel closed after submission
const panelStillVisible = await page.locator('.leaflet-settings-panel').isVisible().catch(() => false);
if (panelStillVisible) {
// Close panel manually
await settingsButton.click();
await page.waitForTimeout(500);
}
} else {
// Already enabled, just close the panel
await settingsButton.click();
await page.waitForTimeout(500);
}
}
/**
* Get current memory usage from browser
*/
async function getMemoryUsage(page) {
return await page.evaluate(() => {
if (window.performance && window.performance.memory) {
return {
usedJSHeapSize: window.performance.memory.usedJSHeapSize,
totalJSHeapSize: window.performance.memory.totalJSHeapSize,
jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit
};
}
// Fallback if performance.memory is not available
return {
usedJSHeapSize: 0,
totalJSHeapSize: 0,
jsHeapSizeLimit: 0
};
});
}
/**
* Simulate a point being received via WebSocket
*/
async function simulatePointReceived(page, pointData) {
await page.evaluate((point) => {
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
if (mapController && mapController.appendPoint) {
// Convert point data to the format expected by appendPoint
const pointArray = [
point.lat, // latitude
point.lng, // longitude
85, // battery
100, // altitude
point.timestamp,// timestamp
0, // velocity
point.id, // id
'DE' // country
];
try {
mapController.appendPoint(pointArray);
} catch (error) {
console.error('Error in appendPoint:', error);
}
} else {
console.warn('Map controller or appendPoint method not found');
}
}, pointData);
}
/**
* Simulate real WebSocket message reception (more realistic)
*/
async function simulateWebSocketMessage(page, pointData) {
const result = await page.evaluate((point) => {
// Find the PointsChannel subscription
const consumer = window.App?.cable || window.consumer;
let debugInfo = {
hasConsumer: !!consumer,
method: 'unknown',
success: false,
error: null
};
if (consumer && consumer.subscriptions) {
const pointsSubscription = consumer.subscriptions.subscriptions.find(sub =>
sub.identifier && JSON.parse(sub.identifier).channel === 'PointsChannel'
);
if (pointsSubscription) {
debugInfo.method = 'websocket';
// Convert point data to the format sent by the server
const serverMessage = [
point.lat, // latitude
point.lng, // longitude
85, // battery
100, // altitude
point.timestamp,// timestamp
0, // velocity
point.id, // id
'DE' // country
];
try {
// Trigger the received callback directly
pointsSubscription.received(serverMessage);
debugInfo.success = true;
} catch (error) {
debugInfo.error = error.message;
console.error('Error in WebSocket message simulation:', error);
}
} else {
debugInfo.method = 'fallback_no_subscription';
// Fallback to direct appendPoint call
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
if (mapController && mapController.appendPoint) {
const pointArray = [point.lat, point.lng, 85, 100, point.timestamp, 0, point.id, 'DE'];
try {
mapController.appendPoint(pointArray);
debugInfo.success = true;
} catch (error) {
debugInfo.error = error.message;
}
} else {
debugInfo.error = 'No map controller found';
}
}
} else {
debugInfo.method = 'fallback_no_consumer';
// Fallback to direct appendPoint call
const mapController = document.querySelector('#map')?._stimulus_controllers?.find(c => c.identifier === 'maps');
if (mapController && mapController.appendPoint) {
const pointArray = [point.lat, point.lng, 85, 100, point.timestamp, 0, point.id, 'DE'];
try {
mapController.appendPoint(pointArray);
debugInfo.success = true;
} catch (error) {
debugInfo.error = error.message;
}
} else {
debugInfo.error = 'No map controller found';
}
}
return debugInfo;
}, pointData);
// Log debug info for first few calls
if (Math.random() < 0.2) { // Log ~20% of calls to avoid spam
console.log('WebSocket simulation result:', result);
}
return result;
}
/**
* Simulate continuous real-time streaming with varying intervals
*/
async function simulateRealtimeStream(page, pointsConfig) {
const {
startLat = 52.520008,
startLng = 13.404954,
pointCount = 20,
maxInterval = 5000, // 5 seconds max between points
minInterval = 100, // 100ms min between points
driftRange = 0.001 // How much coordinates can drift
} = pointsConfig;
let currentLat = startLat;
let currentLng = startLng;
const baseTimestamp = new Date('2025-06-04T12:00:00').getTime();
for (let i = 0; i < pointCount; i++) {
// Simulate GPS drift
currentLat += (Math.random() - 0.5) * driftRange;
currentLng += (Math.random() - 0.5) * driftRange;
// Random interval to simulate real-world timing variations
const interval = Math.random() * (maxInterval - minInterval) + minInterval;
const pointData = {
lat: currentLat,
lng: currentLng,
timestamp: baseTimestamp + (i * 60000), // Base: 1 minute intervals
id: baseTimestamp + i
};
// Use WebSocket simulation for more realistic testing
await simulateWebSocketMessage(page, pointData);
// Wait for the random interval
await page.waitForTimeout(interval);
// Log progress for longer streams
if (i % 5 === 0) {
console.log(`Streamed ${i + 1}/${pointCount} points`);
}
}
}
/**
* Simulate real API-based point creation (most realistic but slower)
*/
async function simulateRealPointStream(page, pointData) {
// Get API key from the page
const apiKey = await page.evaluate(() => {
const mapElement = document.querySelector('#map');
return mapElement?.dataset.api_key;
});
if (!apiKey) {
console.warn('API key not found, falling back to WebSocket simulation');
return await simulateWebSocketMessage(page, pointData);
}
// Create the point via API
const response = await page.evaluate(async (point, key) => {
try {
const response = await fetch('/api/v1/points', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${key}`
},
body: JSON.stringify({
point: {
latitude: point.lat,
longitude: point.lng,
timestamp: new Date(point.timestamp).toISOString(),
battery: 85,
altitude: 100,
velocity: 0
}
})
});
if (response.ok) {
return await response.json();
} else {
console.error(`API call failed: ${response.status}`);
return null;
}
} catch (error) {
console.error('Error creating point via API:', error);
return null;
}
}, pointData, apiKey);
if (response) {
// Wait for the WebSocket message to be processed
await page.waitForTimeout(200);
} else {
// Fallback to WebSocket simulation if API fails
await simulateWebSocketMessage(page, pointData);
}
return response;
}