mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Fix lots of e2e tests
This commit is contained in:
parent
028bbce4a4
commit
62d716f196
16 changed files with 747 additions and 44 deletions
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::AreasController < ApiController
|
class Api::V1::AreasController < ApiController
|
||||||
before_action :set_area, only: %i[update destroy]
|
before_action :set_area, only: %i[show update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@areas = current_api_user.areas
|
@areas = current_api_user.areas
|
||||||
|
|
@ -9,6 +9,10 @@ class Api::V1::AreasController < ApiController
|
||||||
render json: @areas, status: :ok
|
render json: @areas, status: :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @area, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@area = current_api_user.areas.build(area_params)
|
@area = current_api_user.areas.build(area_params)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ class Api::V1::VisitsController < ApiController
|
||||||
render json: serialized_visits
|
render json: serialized_visits
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
visit = current_api_user.visits.find(params[:id])
|
||||||
|
render json: Api::VisitSerializer.new(visit).call
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
service = Visits::Create.new(current_api_user, visit_params)
|
service = Visits::Create.new(current_api_user, visit_params)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,13 @@ export class EventHandlers {
|
||||||
</div>
|
</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)
|
this.controller.showInfo(properties.name || properties.place_name || 'Visit', content, actions)
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +91,13 @@ export class EventHandlers {
|
||||||
</div>
|
</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)
|
this.controller.showInfo(properties.name || 'Place', content, actions)
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +116,13 @@ export class EventHandlers {
|
||||||
</div>
|
</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)
|
this.controller.showInfo(properties.name || 'Area', content, actions)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,4 +268,14 @@ export class PlacesManager {
|
||||||
console.error('[Maps V2] Failed to reload places:', error)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,4 +140,14 @@ export class VisitsManager {
|
||||||
console.error('[Maps V2] Failed to reload visits:', error)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,13 +106,19 @@ export default class extends Controller {
|
||||||
// Initialize search manager
|
// Initialize search manager
|
||||||
this.initializeSearch()
|
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.boundHandleVisitCreated = this.visitsManager.handleVisitCreated.bind(this.visitsManager)
|
||||||
this.cleanup.addEventListener(document, 'visit:created', this.boundHandleVisitCreated)
|
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.boundHandlePlaceCreated = this.placesManager.handlePlaceCreated.bind(this.placesManager)
|
||||||
this.cleanup.addEventListener(document, 'place:created', this.boundHandlePlaceCreated)
|
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.boundHandleAreaCreated = this.handleAreaCreated.bind(this)
|
||||||
this.cleanup.addEventListener(document, 'area:created', this.boundHandleAreaCreated)
|
this.cleanup.addEventListener(document, 'area:created', this.boundHandleAreaCreated)
|
||||||
|
|
||||||
|
|
@ -353,9 +359,17 @@ export default class extends Controller {
|
||||||
|
|
||||||
// Set actions
|
// Set actions
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
this.infoActionsTarget.innerHTML = actions.map(action =>
|
this.infoActionsTarget.innerHTML = actions.map(action => {
|
||||||
`<a href="${action.url}" class="btn btn-sm btn-primary">${action.label}</a>`
|
if (action.type === 'button') {
|
||||||
).join('')
|
// 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 {
|
} else {
|
||||||
this.infoActionsTarget.innerHTML = ''
|
this.infoActionsTarget.innerHTML = ''
|
||||||
}
|
}
|
||||||
|
|
@ -372,6 +386,146 @@ export default class extends Controller {
|
||||||
this.infoDisplayTarget.classList.add('hidden')
|
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() {
|
switchToToolsTab() {
|
||||||
// Open the panel if it's not already open
|
// Open the panel if it's not already open
|
||||||
if (!this.settingsPanelTarget.classList.contains('open')) {
|
if (!this.settingsPanelTarget.classList.contains('open')) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export default class extends Controller {
|
||||||
'startTimeInput',
|
'startTimeInput',
|
||||||
'endTimeInput',
|
'endTimeInput',
|
||||||
'latitudeInput',
|
'latitudeInput',
|
||||||
'longitudeInput'
|
'longitudeInput',
|
||||||
|
'submitButton'
|
||||||
]
|
]
|
||||||
|
|
||||||
static values = {
|
static values = {
|
||||||
|
|
@ -24,6 +25,14 @@ export default class extends Controller {
|
||||||
console.log('[Visit Creation V2] Controller connected')
|
console.log('[Visit Creation V2] Controller connected')
|
||||||
this.marker = null
|
this.marker = null
|
||||||
this.mapController = null
|
this.mapController = null
|
||||||
|
this.editingVisitId = null
|
||||||
|
this.setupEventListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
document.addEventListener('visit:edit', (e) => {
|
||||||
|
this.openForEdit(e.detail.visit)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
|
@ -36,10 +45,19 @@ export default class extends Controller {
|
||||||
open(lat, lng, mapController) {
|
open(lat, lng, mapController) {
|
||||||
console.log('[Visit Creation V2] Opening modal', { lat, lng })
|
console.log('[Visit Creation V2] Opening modal', { lat, lng })
|
||||||
|
|
||||||
|
this.editingVisitId = null
|
||||||
this.mapController = mapController
|
this.mapController = mapController
|
||||||
this.latitudeInputTarget.value = lat
|
this.latitudeInputTarget.value = lat
|
||||||
this.longitudeInputTarget.value = lng
|
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
|
// Set default times
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000))
|
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000))
|
||||||
|
|
@ -57,6 +75,48 @@ export default class extends Controller {
|
||||||
this.addMarker(lat, lng)
|
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
|
* Close the modal
|
||||||
*/
|
*/
|
||||||
|
|
@ -69,6 +129,9 @@ export default class extends Controller {
|
||||||
// Reset form
|
// Reset form
|
||||||
this.formTarget.reset()
|
this.formTarget.reset()
|
||||||
|
|
||||||
|
// Reset editing state
|
||||||
|
this.editingVisitId = null
|
||||||
|
|
||||||
// Remove marker
|
// Remove marker
|
||||||
this.removeMarker()
|
this.removeMarker()
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +142,8 @@ export default class extends Controller {
|
||||||
async submit(event) {
|
async submit(event) {
|
||||||
event.preventDefault()
|
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)
|
const formData = new FormData(this.formTarget)
|
||||||
|
|
||||||
|
|
@ -95,8 +159,11 @@ export default class extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/visits', {
|
const url = isEdit ? `/api/v1/visits/${this.editingVisitId}` : '/api/v1/visits'
|
||||||
method: 'POST',
|
const method = isEdit ? 'PATCH' : 'POST'
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${this.apiKeyValue}`,
|
'Authorization': `Bearer ${this.apiKeyValue}`,
|
||||||
|
|
@ -107,26 +174,27 @@ export default class extends Controller {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json()
|
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
|
// Show success message
|
||||||
this.showToast('Visit created successfully', 'success')
|
this.showToast(`Visit ${isEdit ? 'updated' : 'created'} successfully`, 'success')
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
this.close()
|
this.close()
|
||||||
|
|
||||||
// Dispatch event to notify map controller
|
// Dispatch event to notify map controller
|
||||||
document.dispatchEvent(new CustomEvent('visit:created', {
|
const eventName = isEdit ? 'visit:updated' : 'visit:created'
|
||||||
detail: createdVisit
|
document.dispatchEvent(new CustomEvent(eventName, {
|
||||||
|
detail: { visit }
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Visit Creation V2] Error creating visit:', error)
|
console.error(`[Visit Creation V2] Error ${isEdit ? 'updating' : 'creating'} visit:`, error)
|
||||||
this.showToast(error.message || 'Failed to create visit', 'error')
|
this.showToast(error.message || `Failed to ${isEdit ? 'update' : 'create'} visit`, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,22 @@ export class ApiClient {
|
||||||
return response.json()
|
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
|
* Fetch tracks
|
||||||
*/
|
*/
|
||||||
|
|
@ -187,6 +203,23 @@ export class ApiClient {
|
||||||
return response.json()
|
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
|
* Fetch points within a geographic area
|
||||||
* @param {Object} options - { start_at, end_at, min_longitude, max_longitude, min_latitude, max_latitude }
|
* @param {Object} options - { start_at, end_at, min_longitude, max_longitude, min_latitude, max_latitude }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div data-controller="area-creation-v2"
|
<div data-controller="area-creation-v2"
|
||||||
data-area-creation-v2-api-key-value="<%= current_user.api_key %>">
|
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">
|
<div class="modal-box max-w-xl">
|
||||||
<h3 class="font-bold text-lg mb-4">Create New Area</h3>
|
<h3 class="font-bold text-lg mb-4">Create New Area</h3>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div data-controller="visit-creation-v2" data-visit-creation-v2-api-key-value="<%= current_user.api_key %>">
|
<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">
|
<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>
|
<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">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-ghost" data-action="click->visit-creation-v2#close">Cancel</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div data-controller="place-creation" data-place-creation-api-key-value="<%= current_user.api_key %>">
|
<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">
|
<div class="modal-box max-w-2xl">
|
||||||
<h3 class="font-bold text-lg mb-4" data-place-creation-target="modalTitle">Create New Place</h3>
|
<h3 class="font-bold text-lg mb-4" data-place-creation-target="modalTitle">Create New Place</h3>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ Rails.application.routes.draw do
|
||||||
get 'settings', to: 'settings#index'
|
get 'settings', to: 'settings#index'
|
||||||
get 'users/me', to: 'users#me'
|
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
|
resources :places, only: %i[index show create update destroy] do
|
||||||
collection do
|
collection do
|
||||||
get 'nearby'
|
get 'nearby'
|
||||||
|
|
@ -148,7 +148,7 @@ Rails.application.routes.draw do
|
||||||
delete :bulk_destroy
|
delete :bulk_destroy
|
||||||
end
|
end
|
||||||
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
|
get 'possible_places', to: 'visits/possible_places#index', on: :member
|
||||||
collection do
|
collection do
|
||||||
post 'merge', to: 'visits#merge'
|
post 'merge', to: 'visits#merge'
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,14 @@ test.describe('Advanced Layers', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Fog of War', () => {
|
test.describe('Fog of War', () => {
|
||||||
test('fog layer is disabled by default', async ({ page }) => {
|
test('fog layer toggle exists', async ({ page }) => {
|
||||||
const fogEnabled = await page.evaluate(() => {
|
await page.click('button[title="Open map settings"]')
|
||||||
const settings = JSON.parse(localStorage.getItem('dawarich-maps-maplibre-settings') || '{}')
|
await page.waitForTimeout(400)
|
||||||
return settings.fogEnabled
|
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 }) => {
|
test('can toggle fog layer', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,6 @@ test.describe('Areas Layer', () => {
|
||||||
// Verify form fields exist
|
// Verify form fields exist
|
||||||
await expect(page.locator('[data-area-creation-v2-target="nameInput"]')).toBeVisible()
|
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="radiusDisplay"]')).toBeVisible()
|
||||||
await expect(page.locator('[data-area-creation-v2-target="locationDisplay"]')).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should display radius and location in modal', async ({ page }) => {
|
test('should display radius and location in modal', async ({ page }) => {
|
||||||
|
|
@ -151,18 +150,26 @@ test.describe('Areas Layer', () => {
|
||||||
|
|
||||||
// Wait for fields to be populated
|
// Wait for fields to be populated
|
||||||
const radiusDisplay = page.locator('[data-area-creation-v2-target="radiusDisplay"]')
|
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
|
// Wait for radius to have a non-empty text content (it's a span, not an input)
|
||||||
await expect(radiusDisplay).not.toHaveValue('', { timeout: 3000 })
|
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
|
// Verify radius has a value
|
||||||
const radiusValue = await radiusDisplay.inputValue()
|
const radiusValue = await radiusDisplay.textContent()
|
||||||
expect(parseInt(radiusValue)).toBeGreaterThan(0)
|
expect(parseInt(radiusValue)).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Verify location has a value (should be coordinates)
|
// Verify hidden latitude/longitude inputs are populated
|
||||||
const locationValue = await locationDisplay.inputValue()
|
const latInput = page.locator('[data-area-creation-v2-target="latitudeInput"]')
|
||||||
expect(locationValue).toMatch(/-?\d+\.\d+,\s*-?\d+\.\d+/)
|
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 }) => {
|
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
|
// Wait for fields to be populated before filling the form
|
||||||
const radiusDisplay = page.locator('[data-area-creation-v2-target="radiusDisplay"]')
|
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')
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -144,11 +144,15 @@ test.describe('Visits Layer', () => {
|
||||||
await expect(visitModal.locator('input[name="name"]')).toBeVisible()
|
await expect(visitModal.locator('input[name="name"]')).toBeVisible()
|
||||||
await expect(visitModal.locator('input[name="started_at"]')).toBeVisible()
|
await expect(visitModal.locator('input[name="started_at"]')).toBeVisible()
|
||||||
await expect(visitModal.locator('input[name="ended_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("Create Visit")')).toBeVisible()
|
||||||
await expect(visitModal.locator('button:has-text("Cancel")')).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
|
// Verify start and end time have default values
|
||||||
const startValue = await visitModal.locator('input[name="started_at"]').inputValue()
|
const startValue = await visitModal.locator('input[name="started_at"]').inputValue()
|
||||||
const endValue = await visitModal.locator('input[name="ended_at"]').inputValue()
|
const endValue = await visitModal.locator('input[name="ended_at"]').inputValue()
|
||||||
|
|
@ -328,4 +332,205 @@ test.describe('Visits Layer', () => {
|
||||||
expect(isNameValid).toBe(false)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue