mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
1216 lines
44 KiB
JavaScript
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;
|
|
}
|