mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -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
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue