mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
* 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
461 lines
18 KiB
JavaScript
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)
|
|
})
|
|
})
|
|
})
|