dawarich/e2e/v2/map/layers/visits.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

536 lines
22 KiB
JavaScript

import { test, expect } from '@playwright/test'
import { closeOnboardingModal } from '../../../helpers/navigation.js'
import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../../helpers/setup.js'
/**
* Helper to get the visit creation modal specifically
* There may be multiple modals on the page, so we need to be specific
*/
function getVisitCreationModal(page) {
return page.locator('[data-controller="visit-creation-v2"] .modal-box')
}
test.describe('Visits Layer', () => {
test.beforeEach(async ({ page }) => {
await navigateToMapsV2(page)
await closeOnboardingModal(page)
await waitForMapLibre(page)
await waitForLoadingComplete(page)
await page.waitForTimeout(1500)
})
test.describe('Toggle', () => {
test('visits layer toggle exists', async ({ page }) => {
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await expect(visitsToggle).toBeVisible()
})
test('can toggle visits layer', async ({ page }) => {
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await visitsToggle.check()
await page.waitForTimeout(500)
const isChecked = await visitsToggle.isChecked()
expect(isChecked).toBe(true)
})
})
test.describe('Visit Creation', () => {
test('should show Create a Visit button in Tools tab', async ({ page }) => {
// Open settings panel
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
// Click Tools tab
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
// Verify Create a Visit button exists
const createVisitButton = page.locator('button:has-text("Create a Visit")')
await expect(createVisitButton).toBeVisible()
await expect(createVisitButton).toBeEnabled()
})
test('should enable visit creation mode and show toast', async ({ page }) => {
// Open settings panel and click Tools tab
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
// Click Create a Visit button
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
// Verify settings panel closed
const settingsPanel = page.locator('[data-maps--maplibre-target="settingsPanel"]')
const hasPanelOpenClass = await settingsPanel.evaluate((el) => el.classList.contains('open'))
expect(hasPanelOpenClass).toBe(false)
// Verify toast message appears
const toast = page.locator('.toast:has-text("Click on the map to place a visit")')
await expect(toast).toBeVisible({ timeout: 5000 })
// Verify cursor changed to crosshair
const cursor = await page.evaluate(() => {
const canvas = document.querySelector('.maplibregl-canvas')
return canvas?.style.cursor
})
expect(cursor).toBe('crosshair')
})
test('should open modal when map is clicked', async ({ page }) => {
// Enable visit creation mode
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
// Click on map
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3)
await page.waitForTimeout(2000)
// Verify modal title is visible (modal is open) - this is specific to visit creation modal
await expect(page.locator('h3:has-text("Create New Visit")')).toBeVisible({ timeout: 5000 })
// Verify the specific visit creation modal is visible
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible()
// Verify form has the location coordinates populated
const latInput = visitModal.locator('input[name="latitude"]')
const lngInput = visitModal.locator('input[name="longitude"]')
const latValue = await latInput.inputValue()
const lngValue = await lngInput.inputValue()
expect(latValue).toBeTruthy()
expect(lngValue).toBeTruthy()
})
test('should display correct form fields in modal', async ({ page }) => {
// Enable mode and click map
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3)
await page.waitForTimeout(1500)
// Wait for modal to be visible
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 5000 })
// Verify all form fields exist within the visit creation modal
await expect(visitModal.locator('input[name="name"]')).toBeVisible()
await expect(visitModal.locator('input[name="started_at"]')).toBeVisible()
await expect(visitModal.locator('input[name="ended_at"]')).toBeVisible()
await expect(visitModal.locator('button:has-text("Create Visit")')).toBeVisible()
await expect(visitModal.locator('button:has-text("Cancel")')).toBeVisible()
// Verify hidden coordinate inputs are populated
const latInput = visitModal.locator('input[name="latitude"]')
const lngInput = visitModal.locator('input[name="longitude"]')
await expect(latInput).toHaveValue(/.+/)
await expect(lngInput).toHaveValue(/.+/)
// Verify start and end time have default values
const startValue = await visitModal.locator('input[name="started_at"]').inputValue()
const endValue = await visitModal.locator('input[name="ended_at"]').inputValue()
expect(startValue).toBeTruthy()
expect(endValue).toBeTruthy()
})
test('should close modal when cancel is clicked', async ({ page }) => {
// Enable mode and click map
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(500)
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(500)
// Click Create a Visit button
const createButton = page.locator('button:has-text("Create a Visit")')
await expect(createButton).toBeVisible()
await createButton.click()
await page.waitForTimeout(1000)
// Wait for settings panel to close and cursor to change
await page.waitForTimeout(500)
// Click on map - try a different location
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.5, bbox.y + bbox.height * 0.5)
await page.waitForTimeout(2500)
// Verify modal exists
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 10000 })
// Find the cancel button - it's a ghost button
const cancelButton = visitModal.locator('button.btn-ghost:has-text("Cancel")')
await expect(cancelButton).toBeVisible()
await cancelButton.click()
await page.waitForTimeout(1500)
// Verify modal is closed by checking if modal-open class is removed
const modal = page.locator('[data-controller="visit-creation-v2"] .modal')
const hasModalOpenClass = await modal.evaluate((el) => el.classList.contains('modal-open'))
expect(hasModalOpenClass).toBe(false)
})
test('should create visit successfully', async ({ page }) => {
// Enable visits layer first
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await visitsToggle.check()
await page.waitForTimeout(500)
// Enable visit creation mode
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
// Click on map
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3)
await page.waitForTimeout(2000)
// Wait for modal to be visible
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 5000 })
// Fill form with unique visit name
const visitName = `E2E V2 Test Visit ${Date.now()}`
await visitModal.locator('input[name="name"]').fill(visitName)
// Submit form
await visitModal.locator('button:has-text("Create Visit")').click()
// Wait for success toast - this confirms the visit was created
const successToast = page.locator('.toast:has-text("created successfully")')
await expect(successToast).toBeVisible({ timeout: 10000 })
// Verify modal is closed by checking if modal-open class is removed
await page.waitForTimeout(1500)
const modal = page.locator('[data-controller="visit-creation-v2"] .modal')
const hasModalOpenClass = await modal.evaluate((el) => el.classList.contains('modal-open'))
expect(hasModalOpenClass).toBe(false)
})
test('should make created visit searchable in side panel', async ({ page }) => {
// Enable visits layer
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await visitsToggle.check()
await page.waitForTimeout(500)
// Create a visit with unique name
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3)
await page.waitForTimeout(2000)
// Wait for modal to be visible
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 5000 })
const visitName = `Searchable Visit ${Date.now()}`
await visitModal.locator('input[name="name"]').fill(visitName)
await visitModal.locator('button:has-text("Create Visit")').click()
// Wait for success toast
const successToast = page.locator('.toast:has-text("created successfully")')
await expect(successToast).toBeVisible({ timeout: 10000 })
// Wait for modal to close
await page.waitForTimeout(1500)
// Open settings and go to layers tab to access visit search
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(500)
// Search field should now be visible (bug fix ensures it shows when toggle is checked)
const searchField = page.locator('input#visits-search')
await expect(searchField).toBeVisible({ timeout: 5000 })
// Use the visit search field
await searchField.fill(visitName.substring(0, 10))
await page.waitForTimeout(500)
// Verify the search field is working - just check that it accepted the input
const searchValue = await searchField.inputValue()
expect(searchValue).toBe(visitName.substring(0, 10))
})
test('should validate required fields', async ({ page }) => {
// Enable visit creation mode
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
// Click on map
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3)
await page.waitForTimeout(1500)
// Wait for modal to be visible
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 5000 })
// Clear the name field
await visitModal.locator('input[name="name"]').clear()
// Try to submit form without name
await visitModal.locator('button:has-text("Create Visit")').click()
await page.waitForTimeout(500)
// Verify modal is still open (form validation prevented submission)
const modalVisible = await visitModal.isVisible()
expect(modalVisible).toBe(true)
// Verify name field has validation error (HTML5 validation)
const isNameValid = await visitModal.locator('input[name="name"]').evaluate((el) => el.validity.valid)
expect(isNameValid).toBe(false)
})
})
test.describe('Visit Edit', () => {
test('should open edit modal when clicking Edit in info display', async ({ page }) => {
// Enable visits layer
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await visitsToggle.check()
await page.waitForTimeout(1000)
// Close settings panel
await page.click('button[title="Close panel"]')
await page.waitForTimeout(500)
// Click on a visit marker on the map to trigger info display
// We need to find visits layer features
const hasVisits = 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 visitsLayer = controller?.layerManager?.getLayer('visits')
return visitsLayer?.data?.features?.length > 0
})
if (!hasVisits) {
console.log('No visits found, skipping test')
test.skip()
return
}
// Get a visit feature from the map
const visitId = 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 visitsLayer = controller?.layerManager?.getLayer('visits')
return visitsLayer?.data?.features[0]?.properties?.id
})
if (!visitId) {
console.log('No visit ID found, skipping test')
test.skip()
return
}
// Simulate clicking on a visit to trigger the info display
await page.evaluate((id) => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
// Simulate a visit click event
const mockEvent = {
features: [{
properties: {
id: id,
name: 'Test Visit',
started_at: new Date().toISOString(),
ended_at: new Date().toISOString(),
duration: 3600,
status: 'confirmed'
}
}]
}
controller.eventHandlers.handleVisitClick(mockEvent)
}, visitId)
await page.waitForTimeout(1000)
// Verify info display is shown
const infoDisplay = page.locator('[data-maps--maplibre-target="infoDisplay"]')
await expect(infoDisplay).toBeVisible({ timeout: 5000 })
// Click Edit button
const editButton = infoDisplay.locator('button:has-text("Edit")')
await expect(editButton).toBeVisible()
await editButton.click()
await page.waitForTimeout(1500)
// Verify edit modal opens with "Edit Visit" title
await expect(page.locator('h3:has-text("Edit Visit")')).toBeVisible({ timeout: 5000 })
// Verify the modal has the visit creation controller (now used for editing too)
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible()
// Verify form fields are populated
const nameInput = visitModal.locator('input[name="name"]')
const nameValue = await nameInput.inputValue()
expect(nameValue).toBeTruthy()
})
test('should update visit successfully and refresh map', async ({ page }) => {
// Enable visits layer
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[data-tab="layers"]')
await page.waitForTimeout(300)
const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle')
await visitsToggle.check()
await page.waitForTimeout(1000)
// First create a visit to edit
await page.click('button[data-tab="tools"]')
await page.waitForTimeout(300)
await page.click('button:has-text("Create a Visit")')
await page.waitForTimeout(500)
const mapContainer = page.locator('.maplibregl-canvas')
const bbox = await mapContainer.boundingBox()
await page.mouse.click(bbox.x + bbox.width * 0.4, bbox.y + bbox.height * 0.4)
await page.waitForTimeout(2000)
const visitModal = getVisitCreationModal(page)
await expect(visitModal).toBeVisible({ timeout: 5000 })
const originalName = `Edit Test Visit ${Date.now()}`
await visitModal.locator('input[name="name"]').fill(originalName)
await visitModal.locator('button:has-text("Create Visit")').click()
// Wait for success toast
await expect(page.locator('.toast:has-text("created successfully")')).toBeVisible({ timeout: 10000 })
await page.waitForTimeout(2000)
// Now trigger edit - simulate clicking on the visit
const visitId = await page.evaluate((name) => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const visitsLayer = controller?.layerManager?.getLayer('visits')
const visit = visitsLayer?.data?.features?.find(f => f.properties.name === name)
return visit?.properties?.id
}, originalName)
if (!visitId) {
console.log('Created visit not found in layer, skipping edit test')
test.skip()
return
}
// Simulate clicking on the visit
await page.evaluate((id) => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const mockEvent = {
features: [{
properties: {
id: id,
name: 'Test Visit',
started_at: new Date().toISOString(),
ended_at: new Date().toISOString(),
duration: 3600,
status: 'confirmed'
}
}]
}
controller.eventHandlers.handleVisitClick(mockEvent)
}, visitId)
await page.waitForTimeout(1000)
// Click Edit button
const infoDisplay = page.locator('[data-maps--maplibre-target="infoDisplay"]')
const editButton = infoDisplay.locator('button:has-text("Edit")')
await expect(editButton).toBeVisible({ timeout: 5000 })
await editButton.click()
await page.waitForTimeout(1500)
// Wait for edit modal
await expect(page.locator('h3:has-text("Edit Visit")')).toBeVisible({ timeout: 5000 })
// Update the name
const updatedName = `${originalName} EDITED`
const editModal = getVisitCreationModal(page)
await editModal.locator('input[name="name"]').fill(updatedName)
// Submit the update
await editModal.locator('button:has-text("Update Visit")').click()
// Wait for success toast
await expect(page.locator('.toast:has-text("updated successfully")')).toBeVisible({ timeout: 10000 })
// Wait for modal to close
await page.waitForTimeout(1500)
// Verify the visit was updated in the layer
const visitUpdated = await page.evaluate((name) => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
const app = window.Stimulus || window.Application
const controller = app?.getControllerForElementAndIdentifier(element, 'maps--maplibre')
const visitsLayer = controller?.layerManager?.getLayer('visits')
return visitsLayer?.data?.features?.some(f => f.properties.name === name)
}, updatedName)
expect(visitUpdated).toBe(true)
})
})
})