dawarich/e2e/v2/realtime/live-mode.spec.js
Evgenii Burmakin 8934c29fce
0.36.2 (#2007)
* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>
2025-12-06 20:54:49 +01:00

461 lines
18 KiB
JavaScript

import { test, expect } from '@playwright/test'
import { closeOnboardingModal } from '../../helpers/navigation.js'
import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../helpers/setup.js'
test.describe('Live Mode', () => {
test.beforeEach(async ({ page }) => {
await navigateToMapsV2(page)
await closeOnboardingModal(page)
await waitForMapLibre(page)
await waitForLoadingComplete(page)
// Wait for layers to be fully initialized
await page.waitForFunction(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
if (!element) return false
const app = window.Stimulus || window.Application
if (!app) return false
const controller = app.getControllerForElementAndIdentifier(element, 'maps--maplibre')
return controller?.layerManager?.layers?.recentPointLayer !== undefined
}, { timeout: 10000 })
await page.waitForTimeout(1000)
})
test.describe('Live Mode Toggle', () => {
test('should have live mode toggle in settings', async ({ page }) => {
// Open settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
// Click Settings tab
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
// Verify Live Mode toggle exists
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
await expect(liveModeToggle).toBeVisible()
// Verify label text
const label = page.locator('label:has-text("Live Mode")')
await expect(label).toBeVisible()
// Verify description text
const description = page.locator('text=Show new points in real-time')
await expect(description).toBeVisible()
})
test('should toggle live mode on and off', async ({ page }) => {
// Open settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
// Get initial state
const initialState = await liveModeToggle.isChecked()
// Toggle it
await liveModeToggle.click()
await page.waitForTimeout(500)
// Verify state changed
const newState = await liveModeToggle.isChecked()
expect(newState).toBe(!initialState)
// Toggle back
await liveModeToggle.click()
await page.waitForTimeout(500)
// Verify state reverted
const finalState = await liveModeToggle.isChecked()
expect(finalState).toBe(initialState)
})
test('should show toast notification when toggling live mode', async ({ page }) => {
// Open settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
const initialState = await liveModeToggle.isChecked()
// Toggle and watch for toast
await liveModeToggle.click()
// Wait for toast to appear
const expectedMessage = initialState ? 'Live mode disabled' : 'Live mode enabled'
const toast = page.locator('.toast, [role="alert"]').filter({ hasText: expectedMessage })
await expect(toast).toBeVisible({ timeout: 3000 })
})
})
test.describe('Realtime Controller', () => {
test('should initialize realtime controller when enabled', async ({ page }) => {
const realtimeControllerExists = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return controller !== undefined
})
expect(realtimeControllerExists).toBe(true)
})
test('should have access to maps--maplibre controller', async ({ page }) => {
const hasMapsController = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const realtimeController = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
const mapsController = realtimeController?.mapsV2Controller
return mapsController !== undefined && mapsController.map !== undefined
})
expect(hasMapsController).toBe(true)
})
test('should initialize ActionCable channels', async ({ page }) => {
// Wait for channels to be set up
await page.waitForTimeout(2000)
const channelsInitialized = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return controller?.channels !== undefined
})
expect(channelsInitialized).toBe(true)
})
})
test.describe('Connection Indicator', () => {
test('should have connection indicator element in DOM', async ({ page }) => {
// Connection indicator exists but is hidden by default
const indicator = page.locator('.connection-indicator')
// Should exist in DOM
await expect(indicator).toHaveCount(1)
// Should be hidden (not active) without real ActionCable connection
const isActive = await indicator.evaluate(el => el.classList.contains('active'))
expect(isActive).toBe(false)
})
test('should have connection status classes', async ({ page }) => {
const indicator = page.locator('.connection-indicator')
// Should have disconnected class by default (before connection)
const hasDisconnectedClass = await indicator.evaluate(el =>
el.classList.contains('disconnected')
)
expect(hasDisconnectedClass).toBe(true)
})
test.skip('should show connection indicator when ActionCable connects', async ({ page }) => {
// This test requires actual ActionCable connection
// The indicator becomes visible (.active class added) only when channels connect
// Wait for connection
await page.waitForTimeout(3000)
const indicator = page.locator('.connection-indicator')
// Should be visible with active class
await expect(indicator).toHaveClass(/active/)
await expect(indicator).toBeVisible()
})
test.skip('should show appropriate connection text when active', async ({ page }) => {
// This test requires actual ActionCable connection
// The indicator text shows via CSS ::before pseudo-element
// Wait for connection
await page.waitForTimeout(3000)
const indicatorText = page.locator('.connection-indicator .indicator-text')
// Should show either "Connected" or "Connecting..."
const text = await indicatorText.evaluate(el => {
return window.getComputedStyle(el, '::before').content.replace(/['"]/g, '')
})
expect(['Connected', 'Connecting...']).toContain(text)
})
})
test.describe('Point Handling', () => {
test('should have handleNewPoint method', async ({ page }) => {
const hasMethod = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return typeof controller?.handleNewPoint === 'function'
})
expect(hasMethod).toBe(true)
})
test('should have zoomToPoint method', async ({ page }) => {
const hasMethod = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return typeof controller?.zoomToPoint === 'function'
})
expect(hasMethod).toBe(true)
})
test.skip('should add new point to map when received', async ({ page }) => {
// This test requires actual ActionCable broadcast
// Skipped as it needs backend point creation
// Get initial point count
const initialCount = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const pointsLayer = controller?.layerManager?.getLayer('points')
return pointsLayer?.data?.features?.length || 0
})
// Simulate point broadcast (would need real backend)
// const newPoint = [52.5200, 13.4050, 85, 10, '2025-01-01T12:00:00Z', 5, 999, 'Germany']
// Wait for point to be added
// await page.waitForTimeout(1000)
// Verify point was added
// const newCount = await page.evaluate(() => { ... })
// expect(newCount).toBe(initialCount + 1)
})
test.skip('should zoom to new point location', async ({ page }) => {
// This test requires actual ActionCable broadcast
// Skipped as it needs backend point creation
// Get initial map center
// Broadcast new point at specific location
// Verify map center changed to new point location
})
})
test.describe('Live Mode State Persistence', () => {
test('should maintain live mode state after toggling', async ({ page }) => {
// Open settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
// Enable live mode
if (!await liveModeToggle.isChecked()) {
await liveModeToggle.click()
await page.waitForTimeout(500)
}
// Verify it's enabled
expect(await liveModeToggle.isChecked()).toBe(true)
// Close and reopen settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
// Should still be enabled
expect(await liveModeToggle.isChecked()).toBe(true)
})
})
test.describe('Error Handling', () => {
test('should handle missing maps controller gracefully', async ({ page }) => {
// This is tested by the controller's defensive checks
const hasDefensiveChecks = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
// The controller should have the mapsV2Controller getter
return typeof controller?.mapsV2Controller !== 'undefined'
})
expect(hasDefensiveChecks).toBe(true)
})
test('should handle missing points layer gracefully', async ({ page }) => {
// Console errors should not crash the app
let consoleErrors = []
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text())
}
})
// Wait for initialization
await page.waitForTimeout(2000)
// Should not have critical errors
const hasCriticalErrors = consoleErrors.some(err =>
err.includes('TypeError') || err.includes('Cannot read')
)
expect(hasCriticalErrors).toBe(false)
})
})
test.describe('Recent Point Display', () => {
test('should have recent point layer initialized', async ({ page }) => {
const hasRecentPointLayer = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
return recentPointLayer !== undefined
})
expect(hasRecentPointLayer).toBe(true)
})
test('recent point layer should be hidden by default', async ({ page }) => {
const isHidden = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
return recentPointLayer?.visible === false
})
expect(isHidden).toBe(true)
})
test('recent point layer can be shown programmatically', async ({ page }) => {
// This tests the core functionality: the layer can be made visible
// The toggle integration will work once assets are recompiled
const result = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
if (!recentPointLayer) {
return { success: false, reason: 'layer not found' }
}
// Test that show() works
recentPointLayer.show()
const isVisible = recentPointLayer.visible === true
// Clean up
recentPointLayer.hide()
return { success: isVisible, visible: isVisible }
})
expect(result.success).toBe(true)
})
test('recent point layer can be hidden programmatically', async ({ page }) => {
// This tests the core functionality: the layer can be hidden
const result = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const recentPointLayer = controller?.layerManager?.getLayer('recentPoint')
if (!recentPointLayer) {
return { success: false, reason: 'layer not found' }
}
// Show first, then hide to test the hide functionality
recentPointLayer.show()
recentPointLayer.hide()
const isHidden = recentPointLayer.visible === false
return { success: isHidden, hidden: isHidden }
})
expect(result.success).toBe(true)
})
test('should have updateRecentPoint method', async ({ page }) => {
const hasMethod = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return typeof controller?.updateRecentPoint === 'function'
})
expect(hasMethod).toBe(true)
})
test('should have updateRecentPointLayerVisibility method', async ({ page }) => {
const hasMethod = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre-realtime"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre-realtime')
return typeof controller?.updateRecentPointLayerVisibility === 'function'
})
expect(hasMethod).toBe(true)
})
test.skip('should display recent point when new point is broadcast in live mode', async ({ page }) => {
// This test requires actual ActionCable broadcast
// Skipped as it needs backend point creation
// Open settings and enable live mode
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="settings"]').click()
await page.waitForTimeout(300)
const liveModeToggle = page.locator('[data-maps--maplibre-realtime-target="liveModeToggle"]')
if (!await liveModeToggle.isChecked()) {
await liveModeToggle.click()
await page.waitForTimeout(500)
}
// Close settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
// Disable points layer to test that recent point is still visible
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
await page.locator('button[data-tab="layers"]').click()
await page.waitForTimeout(300)
const pointsToggle = page.locator('[data-action="change->maps--maplibre#togglePoints"]')
if (await pointsToggle.isChecked()) {
await pointsToggle.click()
await page.waitForTimeout(500)
}
// Close settings
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(300)
// Simulate new point broadcast (would need real backend)
// const newPoint = [52.5200, 13.4050, 85, 10, '2025-01-01T12:00:00Z', 5, 999, 'Germany']
// Wait for point to be displayed
// await page.waitForTimeout(1000)
// Verify recent point is visible on map
// const hasRecentPoint = await page.evaluate(() => { ... })
// expect(hasRecentPoint).toBe(true)
})
})
})