Fix lots of e2e tests

This commit is contained in:
Eugene Burmakin 2025-12-06 16:24:11 +01:00
parent 028bbce4a4
commit 62d716f196
16 changed files with 747 additions and 44 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::AreasController < ApiController
before_action :set_area, only: %i[update destroy]
before_action :set_area, only: %i[show update destroy]
def index
@areas = current_api_user.areas
@ -9,6 +9,10 @@ class Api::V1::AreasController < ApiController
render json: @areas, status: :ok
end
def show
render json: @area, status: :ok
end
def create
@area = current_api_user.areas.build(area_params)

View file

@ -10,6 +10,11 @@ class Api::V1::VisitsController < ApiController
render json: serialized_visits
end
def show
visit = current_api_user.visits.find(params[:id])
render json: Api::VisitSerializer.new(visit).call
end
def create
service = Visits::Create.new(current_api_user, visit_params)

View file

@ -49,7 +49,13 @@ export class EventHandlers {
</div>
`
const actions = [{ url: `/visits/${properties.id}`, label: 'View Details →' }]
const actions = [{
type: 'button',
handler: 'handleEdit',
id: properties.id,
entityType: 'visit',
label: 'Edit'
}]
this.controller.showInfo(properties.name || properties.place_name || 'Visit', content, actions)
}
@ -85,7 +91,13 @@ export class EventHandlers {
</div>
`
const actions = properties.id ? [{ url: `/places/${properties.id}`, label: 'View Details →' }] : []
const actions = properties.id ? [{
type: 'button',
handler: 'handleEdit',
id: properties.id,
entityType: 'place',
label: 'Edit'
}] : []
this.controller.showInfo(properties.name || 'Place', content, actions)
}
@ -104,7 +116,13 @@ export class EventHandlers {
</div>
`
const actions = properties.id ? [{ url: `/areas/${properties.id}`, label: 'View Details →' }] : []
const actions = properties.id ? [{
type: 'button',
handler: 'handleDelete',
id: properties.id,
entityType: 'area',
label: 'Delete'
}] : []
this.controller.showInfo(properties.name || 'Area', content, actions)
}

View file

@ -268,4 +268,14 @@ export class PlacesManager {
console.error('[Maps V2] Failed to reload places:', error)
}
}
/**
* Handle place update event - reload places and update layer
*/
async handlePlaceUpdated(event) {
console.log('[Maps V2] Place updated, reloading places...', event.detail)
// Reuse the same logic as creation
await this.handlePlaceCreated(event)
}
}

View file

@ -140,4 +140,14 @@ export class VisitsManager {
console.error('[Maps V2] Failed to reload visits:', error)
}
}
/**
* Handle visit update event - reload visits and update layer
*/
async handleVisitUpdated(event) {
console.log('[Maps V2] Visit updated, reloading visits...', event.detail)
// Reuse the same logic as creation
await this.handleVisitCreated(event)
}
}

View file

@ -106,13 +106,19 @@ export default class extends Controller {
// Initialize search manager
this.initializeSearch()
// Listen for visit and place creation events
// Listen for visit and place creation/update events
this.boundHandleVisitCreated = this.visitsManager.handleVisitCreated.bind(this.visitsManager)
this.cleanup.addEventListener(document, 'visit:created', this.boundHandleVisitCreated)
this.boundHandleVisitUpdated = this.visitsManager.handleVisitUpdated.bind(this.visitsManager)
this.cleanup.addEventListener(document, 'visit:updated', this.boundHandleVisitUpdated)
this.boundHandlePlaceCreated = this.placesManager.handlePlaceCreated.bind(this.placesManager)
this.cleanup.addEventListener(document, 'place:created', this.boundHandlePlaceCreated)
this.boundHandlePlaceUpdated = this.placesManager.handlePlaceUpdated.bind(this.placesManager)
this.cleanup.addEventListener(document, 'place:updated', this.boundHandlePlaceUpdated)
this.boundHandleAreaCreated = this.handleAreaCreated.bind(this)
this.cleanup.addEventListener(document, 'area:created', this.boundHandleAreaCreated)
@ -353,9 +359,17 @@ export default class extends Controller {
// Set actions
if (actions.length > 0) {
this.infoActionsTarget.innerHTML = actions.map(action =>
`<a href="${action.url}" class="btn btn-sm btn-primary">${action.label}</a>`
).join('')
this.infoActionsTarget.innerHTML = actions.map(action => {
if (action.type === 'button') {
// For button actions (modals, etc.), create a button with data-action
// Use error styling for delete buttons
const buttonClass = action.label === 'Delete' ? 'btn btn-sm btn-error' : 'btn btn-sm btn-primary'
return `<button class="${buttonClass}" data-action="click->maps--maplibre#${action.handler}" data-id="${action.id}" data-entity-type="${action.entityType}">${action.label}</button>`
} else {
// For link actions, keep the original behavior
return `<a href="${action.url}" class="btn btn-sm btn-primary">${action.label}</a>`
}
}).join('')
} else {
this.infoActionsTarget.innerHTML = ''
}
@ -372,6 +386,146 @@ export default class extends Controller {
this.infoDisplayTarget.classList.add('hidden')
}
/**
* Handle edit action from info display
*/
handleEdit(event) {
const button = event.currentTarget
const id = button.dataset.id
const entityType = button.dataset.entityType
console.log('[Maps V2] Opening edit for', entityType, id)
switch (entityType) {
case 'visit':
this.openVisitModal(id)
break
case 'place':
this.openPlaceEditModal(id)
break
default:
console.warn('[Maps V2] Unknown entity type:', entityType)
}
}
/**
* Handle delete action from info display
*/
handleDelete(event) {
const button = event.currentTarget
const id = button.dataset.id
const entityType = button.dataset.entityType
console.log('[Maps V2] Deleting', entityType, id)
switch (entityType) {
case 'area':
this.deleteArea(id)
break
default:
console.warn('[Maps V2] Unknown entity type for delete:', entityType)
}
}
/**
* Open visit edit modal
*/
async openVisitModal(visitId) {
try {
// Fetch visit details
const response = await fetch(`/api/v1/visits/${visitId}`, {
headers: {
'Authorization': `Bearer ${this.apiKeyValue}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to fetch visit: ${response.status}`)
}
const visit = await response.json()
// Trigger visit edit event
const event = new CustomEvent('visit:edit', {
detail: { visit },
bubbles: true
})
document.dispatchEvent(event)
} catch (error) {
console.error('[Maps V2] Failed to load visit:', error)
Toast.error('Failed to load visit details')
}
}
/**
* Delete area with confirmation
*/
async deleteArea(areaId) {
try {
// Fetch area details
const area = await this.api.fetchArea(areaId)
// Show delete confirmation
const confirmed = confirm(`Delete area "${area.name}"?\n\nThis action cannot be undone.`)
if (!confirmed) return
Toast.info('Deleting area...')
// Delete the area
await this.api.deleteArea(areaId)
// Reload areas
const areas = await this.api.fetchAreas()
const areasGeoJSON = this.dataLoader.areasToGeoJSON(areas)
const areasLayer = this.layerManager.getLayer('areas')
if (areasLayer) {
areasLayer.update(areasGeoJSON)
}
// Close info display
this.closeInfo()
Toast.success('Area deleted successfully')
} catch (error) {
console.error('[Maps V2] Failed to delete area:', error)
Toast.error('Failed to delete area')
}
}
/**
* Open place edit modal
*/
async openPlaceEditModal(placeId) {
try {
// Fetch place details
const response = await fetch(`/api/v1/places/${placeId}`, {
headers: {
'Authorization': `Bearer ${this.apiKeyValue}`,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error(`Failed to fetch place: ${response.status}`)
}
const place = await response.json()
// Trigger place edit event
const event = new CustomEvent('place:edit', {
detail: { place },
bubbles: true
})
document.dispatchEvent(event)
} catch (error) {
console.error('[Maps V2] Failed to load place:', error)
Toast.error('Failed to load place details')
}
}
switchToToolsTab() {
// Open the panel if it's not already open
if (!this.settingsPanelTarget.classList.contains('open')) {

View file

@ -13,7 +13,8 @@ export default class extends Controller {
'startTimeInput',
'endTimeInput',
'latitudeInput',
'longitudeInput'
'longitudeInput',
'submitButton'
]
static values = {
@ -24,6 +25,14 @@ export default class extends Controller {
console.log('[Visit Creation V2] Controller connected')
this.marker = null
this.mapController = null
this.editingVisitId = null
this.setupEventListeners()
}
setupEventListeners() {
document.addEventListener('visit:edit', (e) => {
this.openForEdit(e.detail.visit)
})
}
disconnect() {
@ -36,10 +45,19 @@ export default class extends Controller {
open(lat, lng, mapController) {
console.log('[Visit Creation V2] Opening modal', { lat, lng })
this.editingVisitId = null
this.mapController = mapController
this.latitudeInputTarget.value = lat
this.longitudeInputTarget.value = lng
// Set modal title and button for creation
if (this.hasModalTitleTarget) {
this.modalTitleTarget.textContent = 'Create New Visit'
}
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.textContent = 'Create Visit'
}
// Set default times
const now = new Date()
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000))
@ -57,6 +75,48 @@ export default class extends Controller {
this.addMarker(lat, lng)
}
/**
* Open the modal for editing an existing visit
*/
openForEdit(visit) {
console.log('[Visit Creation V2] Opening modal for edit', visit)
this.editingVisitId = visit.id
// Set modal title and button for editing
if (this.hasModalTitleTarget) {
this.modalTitleTarget.textContent = 'Edit Visit'
}
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.textContent = 'Update Visit'
}
// Fill form with visit data
this.nameInputTarget.value = visit.name || ''
this.latitudeInputTarget.value = visit.latitude
this.longitudeInputTarget.value = visit.longitude
// Convert timestamps to datetime-local format
this.startTimeInputTarget.value = this.formatDateTime(new Date(visit.started_at))
this.endTimeInputTarget.value = this.formatDateTime(new Date(visit.ended_at))
// Show modal
this.modalTarget.classList.add('modal-open')
// Focus on name input
setTimeout(() => this.nameInputTarget.focus(), 100)
// Try to get map controller from the maps--maplibre controller
const mapElement = document.querySelector('[data-controller*="maps--maplibre"]')
if (mapElement) {
const app = window.Stimulus || window.Application
this.mapController = app?.getControllerForElementAndIdentifier(mapElement, 'maps--maplibre')
}
// Add marker to map
this.addMarker(visit.latitude, visit.longitude)
}
/**
* Close the modal
*/
@ -69,6 +129,9 @@ export default class extends Controller {
// Reset form
this.formTarget.reset()
// Reset editing state
this.editingVisitId = null
// Remove marker
this.removeMarker()
}
@ -79,7 +142,8 @@ export default class extends Controller {
async submit(event) {
event.preventDefault()
console.log('[Visit Creation V2] Submitting form')
const isEdit = this.editingVisitId !== null
console.log(`[Visit Creation V2] Submitting form (${isEdit ? 'edit' : 'create'})`)
const formData = new FormData(this.formTarget)
@ -95,8 +159,11 @@ export default class extends Controller {
}
try {
const response = await fetch('/api/v1/visits', {
method: 'POST',
const url = isEdit ? `/api/v1/visits/${this.editingVisitId}` : '/api/v1/visits'
const method = isEdit ? 'PATCH' : 'POST'
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKeyValue}`,
@ -107,26 +174,27 @@ export default class extends Controller {
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to create visit')
throw new Error(errorData.error || `Failed to ${isEdit ? 'update' : 'create'} visit`)
}
const createdVisit = await response.json()
const visit = await response.json()
console.log('[Visit Creation V2] Visit created successfully', createdVisit)
console.log(`[Visit Creation V2] Visit ${isEdit ? 'updated' : 'created'} successfully`, visit)
// Show success message
this.showToast('Visit created successfully', 'success')
this.showToast(`Visit ${isEdit ? 'updated' : 'created'} successfully`, 'success')
// Close modal
this.close()
// Dispatch event to notify map controller
document.dispatchEvent(new CustomEvent('visit:created', {
detail: createdVisit
const eventName = isEdit ? 'visit:updated' : 'visit:created'
document.dispatchEvent(new CustomEvent(eventName, {
detail: { visit }
}))
} catch (error) {
console.error('[Visit Creation V2] Error creating visit:', error)
this.showToast(error.message || 'Failed to create visit', 'error')
console.error(`[Visit Creation V2] Error ${isEdit ? 'updating' : 'creating'} visit:`, error)
this.showToast(error.message || `Failed to ${isEdit ? 'update' : 'create'} visit`, 'error')
}
}

View file

@ -154,6 +154,22 @@ export class ApiClient {
return response.json()
}
/**
* Fetch single area by ID
* @param {number} areaId - Area ID
*/
async fetchArea(areaId) {
const response = await fetch(`${this.baseURL}/areas/${areaId}`, {
headers: this.getHeaders()
})
if (!response.ok) {
throw new Error(`Failed to fetch area: ${response.statusText}`)
}
return response.json()
}
/**
* Fetch tracks
*/
@ -187,6 +203,23 @@ export class ApiClient {
return response.json()
}
/**
* Delete area by ID
* @param {number} areaId - Area ID
*/
async deleteArea(areaId) {
const response = await fetch(`${this.baseURL}/areas/${areaId}`, {
method: 'DELETE',
headers: this.getHeaders()
})
if (!response.ok) {
throw new Error(`Failed to delete area: ${response.statusText}`)
}
return response.json()
}
/**
* Fetch points within a geographic area
* @param {Object} options - { start_at, end_at, min_longitude, max_longitude, min_latitude, max_latitude }

View file

@ -1,6 +1,6 @@
<div data-controller="area-creation-v2"
data-area-creation-v2-api-key-value="<%= current_user.api_key %>">
<div class="modal" data-area-creation-v2-target="modal">
<div class="modal z-[10000]" data-area-creation-v2-target="modal">
<div class="modal-box max-w-xl">
<h3 class="font-bold text-lg mb-4">Create New Area</h3>

View file

@ -1,5 +1,5 @@
<div data-controller="visit-creation-v2" data-visit-creation-v2-api-key-value="<%= current_user.api_key %>">
<div class="modal" data-visit-creation-v2-target="modal">
<div class="modal z-[10000]" data-visit-creation-v2-target="modal">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4" data-visit-creation-v2-target="modalTitle">Create New Visit</h3>
@ -51,7 +51,7 @@
<div class="modal-action">
<button type="button" class="btn btn-ghost" data-action="click->visit-creation-v2#close">Cancel</button>
<button type="submit" class="btn btn-primary">Create Visit</button>
<button type="submit" class="btn btn-primary" data-visit-creation-v2-target="submitButton">Create Visit</button>
</div>
</form>
</div>

View file

@ -1,5 +1,5 @@
<div data-controller="place-creation" data-place-creation-api-key-value="<%= current_user.api_key %>">
<div class="modal" data-place-creation-target="modal">
<div class="modal z-[10000]" data-place-creation-target="modal">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4" data-place-creation-target="modalTitle">Create New Place</h3>

View file

@ -132,7 +132,7 @@ Rails.application.routes.draw do
get 'settings', to: 'settings#index'
get 'users/me', to: 'users#me'
resources :areas, only: %i[index create update destroy]
resources :areas, only: %i[index show create update destroy]
resources :places, only: %i[index show create update destroy] do
collection do
get 'nearby'
@ -148,7 +148,7 @@ Rails.application.routes.draw do
delete :bulk_destroy
end
end
resources :visits, only: %i[index create update destroy] do
resources :visits, only: %i[index show create update destroy] do
get 'possible_places', to: 'visits/possible_places#index', on: :member
collection do
post 'merge', to: 'visits#merge'

View file

@ -14,13 +14,14 @@ test.describe('Advanced Layers', () => {
})
test.describe('Fog of War', () => {
test('fog layer is disabled by default', async ({ page }) => {
const fogEnabled = await page.evaluate(() => {
const settings = JSON.parse(localStorage.getItem('dawarich-maps-maplibre-settings') || '{}')
return settings.fogEnabled
})
test('fog 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)
expect(fogEnabled).toBeFalsy()
const fogToggle = page.locator('label:has-text("Fog of War")').first().locator('input.toggle')
await expect(fogToggle).toBeVisible()
})
test('can toggle fog layer', async ({ page }) => {

View file

@ -128,7 +128,6 @@ test.describe('Areas Layer', () => {
// Verify form fields exist
await expect(page.locator('[data-area-creation-v2-target="nameInput"]')).toBeVisible()
await expect(page.locator('[data-area-creation-v2-target="radiusDisplay"]')).toBeVisible()
await expect(page.locator('[data-area-creation-v2-target="locationDisplay"]')).toBeVisible()
})
test('should display radius and location in modal', async ({ page }) => {
@ -151,18 +150,26 @@ test.describe('Areas Layer', () => {
// Wait for fields to be populated
const radiusDisplay = page.locator('[data-area-creation-v2-target="radiusDisplay"]')
const locationDisplay = page.locator('[data-area-creation-v2-target="locationDisplay"]')
// Wait for radius to have a non-empty value
await expect(radiusDisplay).not.toHaveValue('', { timeout: 3000 })
// Wait for radius to have a non-empty text content (it's a span, not an input)
await page.waitForFunction(() => {
const elem = document.querySelector('[data-area-creation-v2-target="radiusDisplay"]')
return elem && elem.textContent && elem.textContent !== '0'
}, { timeout: 3000 })
// Verify radius has a value
const radiusValue = await radiusDisplay.inputValue()
const radiusValue = await radiusDisplay.textContent()
expect(parseInt(radiusValue)).toBeGreaterThan(0)
// Verify location has a value (should be coordinates)
const locationValue = await locationDisplay.inputValue()
expect(locationValue).toMatch(/-?\d+\.\d+,\s*-?\d+\.\d+/)
// Verify hidden latitude/longitude inputs are populated
const latInput = page.locator('[data-area-creation-v2-target="latitudeInput"]')
const lngInput = page.locator('[data-area-creation-v2-target="longitudeInput"]')
const latValue = await latInput.inputValue()
const lngValue = await lngInput.inputValue()
expect(parseFloat(latValue)).not.toBeNaN()
expect(parseFloat(lngValue)).not.toBeNaN()
})
test('should create area and enable layer when submitted', async ({ page }) => {
@ -185,7 +192,11 @@ test.describe('Areas Layer', () => {
// Wait for fields to be populated before filling the form
const radiusDisplay = page.locator('[data-area-creation-v2-target="radiusDisplay"]')
await expect(radiusDisplay).not.toHaveValue('', { timeout: 3000 })
// Wait for radius to have a non-empty text content (it's a span, not an input)
await page.waitForFunction(() => {
const elem = document.querySelector('[data-area-creation-v2-target="radiusDisplay"]')
return elem && elem.textContent && elem.textContent !== '0'
}, { timeout: 3000 })
await page.locator('[data-area-creation-v2-target="nameInput"]').fill('Test Area E2E')
@ -238,4 +249,188 @@ test.describe('Areas Layer', () => {
}
})
})
test.describe('Area Deletion', () => {
test('should show Delete button when clicking on an area', async ({ page }) => {
// Enable areas layer first
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(200)
await page.locator('button[data-tab="layers"]').click()
await page.waitForTimeout(200)
const areasToggle = page.locator('label:has-text("Areas")').first().locator('input.toggle')
await areasToggle.check()
await page.waitForTimeout(1000)
// Close settings
await page.click('button[title="Close panel"]')
await page.waitForTimeout(500)
// Check if there are any areas
const hasAreas = 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 areasLayer = controller?.layerManager?.getLayer('areas')
return areasLayer?.data?.features?.length > 0
})
if (!hasAreas) {
console.log('No areas found, skipping test')
test.skip()
return
}
// Get an area ID
const areaId = 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 areasLayer = controller?.layerManager?.getLayer('areas')
return areasLayer?.data?.features[0]?.properties?.id
})
if (!areaId) {
console.log('No area ID found, skipping test')
test.skip()
return
}
// Simulate clicking on an area
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 Area',
radius: 500,
latitude: 40.7128,
longitude: -74.0060
}
}]
}
controller.eventHandlers.handleAreaClick(mockEvent)
}, areaId)
await page.waitForTimeout(1000)
// Verify info display is shown
const infoDisplay = page.locator('[data-maps--maplibre-target="infoDisplay"]')
await expect(infoDisplay).toBeVisible({ timeout: 5000 })
// Verify Delete button exists and has error styling (red)
const deleteButton = infoDisplay.locator('button:has-text("Delete")')
await expect(deleteButton).toBeVisible()
await expect(deleteButton).toHaveClass(/btn-error/)
})
test('should delete area with confirmation and update map', async ({ page }) => {
// First create an area to delete
await page.locator('[data-action="click->maps--maplibre#toggleSettings"]').first().click()
await page.waitForTimeout(200)
await page.locator('button[data-tab="tools"]').click()
await page.waitForTimeout(200)
await page.locator('button:has-text("Create an Area")').click()
await page.waitForTimeout(500)
const mapCanvas = page.locator('.maplibregl-canvas')
await mapCanvas.click({ position: { x: 400, y: 300 } })
await page.waitForTimeout(300)
await mapCanvas.click({ position: { x: 450, y: 350 } })
const areaModal = page.locator('[data-area-creation-v2-target="modal"]')
await expect(areaModal).toHaveClass(/modal-open/, { timeout: 5000 })
const radiusDisplay = page.locator('[data-area-creation-v2-target="radiusDisplay"]')
// Wait for radius to have a non-empty text content (it's a span, not an input)
await page.waitForFunction(() => {
const elem = document.querySelector('[data-area-creation-v2-target="radiusDisplay"]')
return elem && elem.textContent && elem.textContent !== '0'
}, { timeout: 3000 })
const areaName = `Delete Test Area ${Date.now()}`
await page.locator('[data-area-creation-v2-target="nameInput"]').fill(areaName)
// Click the submit button specifically in the area creation modal
await page.locator('[data-area-creation-v2-target="submitButton"]').click()
// Wait for creation success
await expect(page.locator('.toast:has-text("successfully")')).toBeVisible({ timeout: 10000 })
await page.waitForTimeout(2000)
// Get the created area ID
const areaId = 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 areasLayer = controller?.layerManager?.getLayer('areas')
const area = areasLayer?.data?.features?.find(f => f.properties.name === name)
return area?.properties?.id
}, areaName)
if (!areaId) {
console.log('Created area not found in layer, skipping delete test')
test.skip()
return
}
// Simulate clicking on the area
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 Area',
radius: 500,
latitude: 40.7128,
longitude: -74.0060
}
}]
}
controller.eventHandlers.handleAreaClick(mockEvent)
}, areaId)
await page.waitForTimeout(1000)
// Setup confirmation dialog handler before clicking delete
const dialogPromise = page.waitForEvent('dialog')
// Click Delete button
const infoDisplay = page.locator('[data-maps--maplibre-target="infoDisplay"]')
const deleteButton = infoDisplay.locator('button:has-text("Delete")')
await expect(deleteButton).toBeVisible({ timeout: 5000 })
await deleteButton.click()
// Handle the confirmation dialog
const dialog = await dialogPromise
expect(dialog.message()).toContain('Delete area')
await dialog.accept()
// Wait for deletion toast
await expect(page.locator('.toast:has-text("deleted successfully")')).toBeVisible({ timeout: 10000 })
// Verify the area was removed from the layer
await page.waitForTimeout(1500)
const areaStillExists = 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 areasLayer = controller?.layerManager?.getLayer('areas')
return areasLayer?.data?.features?.some(f => f.properties.name === name)
}, areaName)
expect(areaStillExists).toBe(false)
// Verify info display is closed
await expect(infoDisplay).not.toBeVisible()
})
})
})

View file

@ -144,11 +144,15 @@ test.describe('Visits Layer', () => {
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('input[data-visit-creation-v2-target="locationDisplay"]')).toBeVisible()
await expect(visitModal.locator('button:has-text("Adjust")')).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()
@ -328,4 +332,205 @@ test.describe('Visits Layer', () => {
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)
})
})
})