Add more tests

This commit is contained in:
Eugene Burmakin 2025-11-05 21:09:37 +01:00
parent bf96acf92e
commit 07224723ed
21 changed files with 2633 additions and 1081 deletions

View file

@ -1,209 +0,0 @@
# Bulk Delete Points Feature - Summary
## Overview
Added a bulk delete feature that allows users to select multiple points on the map by drawing a rectangle and delete them all at once, with confirmation and without page reload.
## Changes Made
### Backend (API)
1. **app/controllers/api/v1/points_controller.rb**
- Added `bulk_destroy` to authentication (`before_action`) on line 4
- Added `bulk_destroy` action (lines 48-59) that:
- Accepts `point_ids` array parameter
- Validates that points exist
- Deletes points belonging to current user
- Returns JSON with success message and count
- Added `bulk_destroy_params` private method (lines 71-73) to permit `point_ids` array
2. **config/routes.rb** (lines 127-131)
- Added `DELETE /api/v1/points/bulk_destroy` collection route
### Frontend
3. **app/javascript/maps/visits.js**
- **Import** (line 3): Added `createPolylinesLayer` import from `./polylines`
- **Constructor** (line 8): Added `mapsController` parameter to receive maps controller reference
- **Selection UI** (lines 389-427): Updated `addSelectionCancelButton()` to add:
- "Cancel Selection" button (warning style)
- "Delete Points" button (error/danger style) with:
- Trash icon SVG
- Point count badge showing number of selected points
- Both buttons in flex container
- **Delete Logic** (lines 432-529): Added `deleteSelectedPoints()` async method:
- Extracts point IDs from `this.selectedPoints` array at index 6 (not 2!)
- Shows confirmation dialog with warning message
- Makes DELETE request to `/api/v1/points/bulk_destroy` with Bearer token auth
- On success:
- Removes markers from map via `mapsController.removeMarker()`
- Updates polylines layer
- Updates heatmap with remaining points
- Updates fog layer if enabled
- Clears selection and removes buttons
- Shows success flash message
- On error: Shows error flash message
- **Polylines Update** (lines 534-577): Added `updatePolylinesAfterDeletion()` helper method:
- Checks if polylines layer was visible before deletion
- Removes old polylines layer
- Creates new polylines layer with updated markers
- Re-adds to map ONLY if it was visible before (preserves layer state)
- Updates layer control with new polylines reference
4. **app/javascript/controllers/maps_controller.js** (line 211)
- Pass `this` (maps controller reference) when creating VisitsManager
- Enables VisitsManager to call maps controller methods like `removeMarker()`, `updateFog()`, etc.
## Technical Details
### Point ID Extraction
The point array structure is:
```javascript
[lat, lng, ?, ?, timestamp, ?, id, country, ?]
0 1 2 3 4 5 6 7 8
```
So point ID is at **index 6**, not index 2!
### API Request Format
```javascript
DELETE /api/v1/points/bulk_destroy
Headers:
Authorization: Bearer {apiKey}
Content-Type: application/json
X-CSRF-Token: {token}
Body:
{
"point_ids": ["123", "456", "789"]
}
```
### API Response Format
Success (200):
```json
{
"message": "Points were successfully destroyed",
"count": 3
}
```
Error (422):
```json
{
"error": "No points selected"
}
```
### Map Updates Without Page Reload
After deletion, the following map elements are updated:
1. **Markers**: Removed via `mapsController.removeMarker(id)` for each deleted point
2. **Polylines/Routes**: Recreated with remaining points, preserving visibility state
3. **Heatmap**: Updated with `setLatLngs()` using remaining markers
4. **Fog of War**: Recalculated if layer is enabled
5. **Layer Control**: Rebuilt to reflect updated layers
6. **Selection**: Cleared (rectangle removed, buttons hidden)
### Layer State Preservation
The Routes layer visibility is preserved after deletion:
- If Routes was **enabled** before deletion → stays enabled
- If Routes was **disabled** before deletion → stays disabled
This is achieved by:
1. Checking `map.hasLayer(polylinesLayer)` before deletion
2. Storing state in `wasPolyLayerVisible` boolean
3. Only calling `polylinesLayer.addTo(map)` if it was visible
4. Explicitly calling `map.removeLayer(polylinesLayer)` if it was NOT visible
## User Experience
### Workflow
1. User clicks area selection tool button (square with dashed border icon)
2. Selection mode activates (map dragging disabled)
3. User draws rectangle by clicking and dragging on map
4. On mouse up:
- Rectangle finalizes
- Points within bounds are selected
- Visits drawer shows selected visits
- Two buttons appear at top of drawer:
- "Cancel Selection" (yellow/warning)
- "Delete Points" with count badge (red/error)
5. User clicks "Delete Points" button
6. Warning confirmation dialog appears:
```
⚠️ WARNING: This will permanently delete X points from your location history.
This action cannot be undone!
Are you sure you want to continue?
```
7. If confirmed:
- Points deleted via API
- Map updates without reload
- Success message: "Successfully deleted X points"
- Selection cleared automatically
8. If canceled:
- No action taken
- Dialog closes
### UI Elements
- **Area Selection Button**: Located in top-right corner of map, shows dashed square icon
- **Cancel Button**: Yellow/warning styled, full width in drawer
- **Delete Button**: Red/error styled, shows trash icon + count badge
- **Count Badge**: Small badge showing number of selected points (e.g., "5")
- **Flash Messages**: Success (green) or error (red) notifications
## Testing
### Playwright Tests (e2e/bulk-delete-points.spec.js)
Created 12 comprehensive tests covering:
1. ✅ Area selection button visibility
2. ✅ Selection mode activation
3. ⏳ Point selection and delete button appearance (needs debugging)
4. ⏳ Point count badge display (needs debugging)
5. ⏳ Cancel/Delete button pair (needs debugging)
6. ⏳ Cancel functionality (needs debugging)
7. ⏳ Confirmation dialog (needs debugging)
8. ⏳ Successful deletion with flash message (needs debugging)
9. ⏳ Routes layer state preservation when disabled (needs debugging)
10. ⏳ Routes layer state preservation when enabled (needs debugging)
11. ⏳ Heatmap update after deletion (needs debugging)
12. ⏳ Selection cleanup after deletion (needs debugging)
**Note**: Tests 1-3 pass, but tests involving the delete button are timing out. This may be due to:
- Points not being selected properly in test environment
- Drawer not opening
- Different date range needed
- Need to wait for visits API call to complete
### Manual Testing Verified
- ✅ Area selection tool activation
- ✅ Rectangle drawing
- ✅ Point selection
- ✅ Delete button with count badge
- ✅ Confirmation dialog
- ✅ Successful deletion
- ✅ Map updates without reload
- ✅ Routes layer visibility preservation
- ✅ Heatmap updates
- ✅ Success flash messages
## Security Considerations
- ✅ API endpoint requires authentication (`authenticate_active_api_user!`)
- ✅ Points are scoped to `current_api_user.points` (can't delete other users' points)
- ✅ Strong parameters used to permit only `point_ids` array
- ✅ CSRF token included in request headers
- ✅ Confirmation dialog prevents accidental deletion
- ✅ Warning message clearly states action is irreversible
## Performance Considerations
- Bulk deletion is more efficient than individual deletes (single API call)
- Map updates are batched (all markers removed, then layers updated once)
- No page reload means faster UX
- Potential improvement: Add loading indicator for large deletions
## Future Enhancements
- [ ] Add loading indicator during deletion
- [ ] Add "Undo" functionality (would require soft deletes)
- [ ] Allow selection of individual points within rectangle (checkbox per point)
- [ ] Add keyboard shortcuts (Delete key to delete selected points)
- [ ] Add selection stats in drawer header (e.g., "15 points selected, 2.3 km total distance")
- [ ] Support polygon selection (not just rectangle)
- [ ] Add "Select All Points" button for current date range

View file

@ -127,6 +127,7 @@ export default class extends Controller {
}
exitAddVisitMode(button) {
console.log('exitAddVisitMode called');
this.isAddingVisit = false;
// Reset button style to inactive state
@ -140,14 +141,20 @@ export default class extends Controller {
// Remove any existing marker
if (this.addVisitMarker) {
console.log('Removing add visit marker');
this.map.removeLayer(this.addVisitMarker);
this.addVisitMarker = null;
}
// Close any open popup
if (this.currentPopup) {
console.log('Closing current popup');
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
} else {
console.warn('No currentPopup reference found');
// Fallback: try to close any open popup
this.map.closePopup();
}
}
@ -263,7 +270,10 @@ export default class extends Controller {
}
if (cancelButton) {
cancelButton.addEventListener('click', () => {
cancelButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('Cancel button clicked - exiting add visit mode');
this.exitAddVisitMode(this.addVisitButton);
});
}

View file

@ -229,7 +229,10 @@ export class VisitsManager {
// Add cancel selection button to the drawer AFTER displayVisits
// This needs to be after because displayVisits sets innerHTML which would wipe out the buttons
this.addSelectionCancelButton();
// Use setTimeout to ensure DOM has fully updated
setTimeout(() => {
this.addSelectionCancelButton();
}, 0);
} catch (error) {
console.error('Error fetching visits in selection:', error);
@ -390,15 +393,18 @@ export class VisitsManager {
* Adds a cancel button to the drawer to clear the selection
*/
addSelectionCancelButton() {
console.log('addSelectionCancelButton: Called');
const container = document.getElementById('visits-list');
if (!container) {
console.warn('addSelectionCancelButton: visits-list container not found');
console.error('addSelectionCancelButton: visits-list container not found');
return;
}
console.log('addSelectionCancelButton: Container found');
// Remove any existing button container first to avoid duplicates
const existingButtonContainer = container.querySelector('.flex.gap-2.mb-4');
const existingButtonContainer = document.getElementById('selection-button-container');
if (existingButtonContainer) {
console.log('addSelectionCancelButton: Removing existing button container');
existingButtonContainer.remove();
}
@ -427,9 +433,9 @@ export class VisitsManager {
badge.className = 'badge badge-sm ml-1';
badge.textContent = this.selectedPoints.length;
deleteButton.appendChild(badge);
console.log(`addSelectionCancelButton: Added button with badge showing ${this.selectedPoints.length} points`);
console.log(`addSelectionCancelButton: Added badge with ${this.selectedPoints.length} points`);
} else {
console.warn('addSelectionCancelButton: No selected points found');
console.warn('addSelectionCancelButton: No selected points, selectedPoints =', this.selectedPoints);
}
buttonContainer.appendChild(cancelButton);
@ -437,7 +443,15 @@ export class VisitsManager {
// Insert at the beginning of the container
container.insertBefore(buttonContainer, container.firstChild);
console.log('addSelectionCancelButton: Buttons added successfully');
console.log('addSelectionCancelButton: Buttons inserted into DOM');
// Verify buttons are in DOM
setTimeout(() => {
const verifyDelete = document.getElementById('delete-selection-button');
const verifyCancel = document.getElementById('cancel-selection-button');
console.log('addSelectionCancelButton: Verification - Delete button exists:', !!verifyDelete);
console.log('addSelectionCancelButton: Verification - Cancel button exists:', !!verifyCancel);
}, 100);
}
/**
@ -617,7 +631,8 @@ export class VisitsManager {
});
// Update the drawer content if it's being opened - but don't fetch visits automatically
if (this.drawerOpen) {
// Only show the "no data" message if there's no selection active
if (this.drawerOpen && !this.isSelectionActive) {
const container = document.getElementById('visits-list');
if (container) {
container.innerHTML = `

115
e2e/README.md Normal file
View file

@ -0,0 +1,115 @@
# E2E Tests
End-to-end tests for Dawarich using Playwright.
## Running Tests
```bash
# Run all tests
npx playwright test
# Run specific test file
npx playwright test e2e/map/map-controls.spec.js
# Run tests in headed mode (watch browser)
npx playwright test --headed
# Run tests in debug mode
npx playwright test --debug
# Run tests sequentially (avoid parallel issues)
npx playwright test --workers=1
```
## Structure
```
e2e/
├── setup/ # Test setup and authentication
├── helpers/ # Shared helper functions
├── map/ # Map-related tests (40 tests total)
└── temp/ # Playwright artifacts (screenshots, videos)
```
### Test Files
**Map Tests (62 tests)**
- `map-controls.spec.js` - Basic map controls, zoom, tile layers (5 tests)
- `map-layers.spec.js` - Layer toggles: Routes, Heatmap, Fog, etc. (8 tests)
- `map-points.spec.js` - Point interactions and deletion (4 tests)
- `map-visits.spec.js` - Confirmed visit interactions and management (5 tests)
- `map-suggested-visits.spec.js` - Suggested visit interactions (confirm/decline) (6 tests)
- `map-add-visit.spec.js` - Add visit control and form (8 tests)
- `map-selection-tool.spec.js` - Selection tool functionality (4 tests)
- `map-calendar-panel.spec.js` - Calendar panel navigation (9 tests)
- `map-side-panel.spec.js` - Side panel (visits drawer) functionality (13 tests)*
- `map-bulk-delete.spec.js` - Bulk point deletion (12 tests)
\* Some side panel tests may be skipped if demo data doesn't contain visits
## Helper Functions
### Map Helpers (`helpers/map.js`)
- `waitForMap(page)` - Wait for Leaflet map initialization
- `enableLayer(page, layerName)` - Enable a map layer by name
- `clickConfirmedVisit(page)` - Click first confirmed visit circle
- `clickSuggestedVisit(page)` - Click first suggested visit circle
- `getMapZoom(page)` - Get current map zoom level
### Navigation Helpers (`helpers/navigation.js`)
- `closeOnboardingModal(page)` - Close getting started modal
- `navigateToDate(page, startDate, endDate)` - Navigate to specific date range
- `navigateToMap(page)` - Navigate to map page with setup
### Selection Helpers (`helpers/selection.js`)
- `drawSelectionRectangle(page, options)` - Draw selection on map
- `enableSelectionMode(page)` - Enable area selection tool
## Common Patterns
### Basic Test Template
```javascript
import { test, expect } from '@playwright/test';
import { navigateToMap } from '../helpers/navigation.js';
import { waitForMap } from '../helpers/map.js';
test('my test', async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
// Your test logic
});
```
### Testing Map Layers
```javascript
import { enableLayer } from '../helpers/map.js';
await enableLayer(page, 'Routes');
await enableLayer(page, 'Heatmap');
```
## Debugging
### View Test Artifacts
```bash
# Open HTML report
npx playwright show-report
# Screenshots and videos are in:
test-results/
```
### Common Issues
- **Flaky tests**: Run with `--workers=1` to avoid parallel interference
- **Timeout errors**: Increase timeout in test or use `page.waitForTimeout()`
- **Map not loading**: Ensure `waitForMap()` is called after navigation
## CI/CD
Tests run with:
- 1 worker (sequential)
- 2 retries on failure
- Screenshots/videos on failure
- JUnit XML reports
See `playwright.config.js` for full configuration.

84
e2e/helpers/map.js Normal file
View file

@ -0,0 +1,84 @@
/**
* Map helper functions for Playwright tests
*/
/**
* Wait for Leaflet map to be fully initialized
* @param {Page} page - Playwright page object
*/
export async function waitForMap(page) {
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
}
/**
* Enable a map layer by name
* @param {Page} page - Playwright page object
* @param {string} layerName - Name of the layer to enable (e.g., "Routes", "Heatmap")
*/
export async function enableLayer(page, layerName) {
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const checkbox = page.locator(`.leaflet-control-layers-overlays label:has-text("${layerName}") input[type="checkbox"]`);
const isChecked = await checkbox.isChecked();
if (!isChecked) {
await checkbox.check();
await page.waitForTimeout(1000);
}
}
/**
* Click on the first confirmed visit circle on the map
* @param {Page} page - Playwright page object
* @returns {Promise<boolean>} - True if a visit was clicked, false otherwise
*/
export async function clickConfirmedVisit(page) {
return await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit) {
firstVisit.fire('click');
return true;
}
}
return false;
});
}
/**
* Click on the first suggested visit circle on the map
* @param {Page} page - Playwright page object
* @returns {Promise<boolean>} - True if a visit was clicked, false otherwise
*/
export async function clickSuggestedVisit(page) {
return await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
const layers = controller.visitsManager.suggestedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit) {
firstVisit.fire('click');
return true;
}
}
return false;
});
}
/**
* Get current map zoom level
* @param {Page} page - Playwright page object
* @returns {Promise<number|null>} - Current zoom level or null
*/
export async function getMapZoom(page) {
return await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.map?.getZoom() || null;
});
}

45
e2e/helpers/navigation.js Normal file
View file

@ -0,0 +1,45 @@
/**
* Navigation and UI helper functions for Playwright tests
*/
/**
* Close the onboarding modal if it's open
* @param {Page} page - Playwright page object
*/
export async function closeOnboardingModal(page) {
const onboardingModal = page.locator('#getting_started');
const isModalOpen = await onboardingModal.evaluate((dialog) => dialog.open).catch(() => false);
if (isModalOpen) {
await page.locator('#getting_started button.btn-primary').click();
await page.waitForTimeout(500);
}
}
/**
* Navigate to the map page and close onboarding modal
* @param {Page} page - Playwright page object
*/
export async function navigateToMap(page) {
await page.goto('/map');
await closeOnboardingModal(page);
}
/**
* Navigate to a specific date range on the map
* @param {Page} page - Playwright page object
* @param {string} startDate - Start date in format 'YYYY-MM-DDTHH:mm'
* @param {string} endDate - End date in format 'YYYY-MM-DDTHH:mm'
*/
export async function navigateToDate(page, startDate, endDate) {
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill(startDate);
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill(endDate);
await page.click('input[type="submit"][value="Search"]');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
}

64
e2e/helpers/selection.js Normal file
View file

@ -0,0 +1,64 @@
/**
* Selection and drawing helper functions for Playwright tests
*/
/**
* Enable selection mode by clicking the selection tool button
* @param {Page} page - Playwright page object
*/
export async function enableSelectionMode(page) {
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
}
/**
* Draw a selection rectangle on the map
* @param {Page} page - Playwright page object
* @param {Object} options - Drawing options
* @param {number} options.startX - Start X position (0-1 as fraction of width, default: 0.2)
* @param {number} options.startY - Start Y position (0-1 as fraction of height, default: 0.2)
* @param {number} options.endX - End X position (0-1 as fraction of width, default: 0.8)
* @param {number} options.endY - End Y position (0-1 as fraction of height, default: 0.8)
* @param {number} options.steps - Number of steps for smooth drag (default: 10)
*/
export async function drawSelectionRectangle(page, options = {}) {
const {
startX = 0.2,
startY = 0.2,
endX = 0.8,
endY = 0.8,
steps = 10
} = options;
// Click area selection tool
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
// Get map container bounding box
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
// Calculate absolute positions
const absStartX = bbox.x + bbox.width * startX;
const absStartY = bbox.y + bbox.height * startY;
const absEndX = bbox.x + bbox.width * endX;
const absEndY = bbox.y + bbox.height * endY;
// Draw rectangle
await page.mouse.move(absStartX, absStartY);
await page.mouse.down();
await page.mouse.move(absEndX, absEndY, { steps });
await page.mouse.up();
// Wait for API calls and drawer animations
await page.waitForTimeout(2000);
// Wait for drawer to open (it should open automatically after selection)
await page.waitForSelector('#visits-drawer.open', { timeout: 15000 });
// Wait for delete button to appear in the drawer (indicates selection is complete)
await page.waitForSelector('#delete-selection-button', { timeout: 15000 });
await page.waitForTimeout(500); // Brief wait for UI to stabilize
}

View file

@ -1,795 +0,0 @@
import { test, expect } from '@playwright/test';
// Helper function to wait for map initialization
async function waitForMap(page) {
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
}
// Helper function to close onboarding modal
async function closeOnboardingModal(page) {
const onboardingModal = page.locator('#getting_started');
const isModalOpen = await onboardingModal.evaluate((dialog) => dialog.open).catch(() => false);
if (isModalOpen) {
await page.locator('#getting_started button.btn-primary').click();
await page.waitForTimeout(500);
}
}
// Helper function to enable a layer by name
async function enableLayer(page, layerName) {
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const checkbox = page.locator(`.leaflet-control-layers-overlays label:has-text("${layerName}") input[type="checkbox"]`);
const isChecked = await checkbox.isChecked();
if (!isChecked) {
await checkbox.check();
await page.waitForTimeout(1000);
}
}
// Helper function to click on a confirmed visit
async function clickConfirmedVisit(page) {
return await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit) {
firstVisit.fire('click');
return true;
}
}
return false;
});
}
test.describe('Map Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/map');
await closeOnboardingModal(page);
});
test('should load map container and display map with controls', async ({ page }) => {
await expect(page.locator('#map')).toBeVisible();
await waitForMap(page);
// Verify zoom controls are present
await expect(page.locator('.leaflet-control-zoom')).toBeVisible();
// Verify custom map controls are present (from map_controls.js)
await expect(page.locator('.add-visit-button')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.toggle-panel-button')).toBeVisible();
await expect(page.locator('.drawer-button')).toBeVisible();
await expect(page.locator('#selection-tool-button')).toBeVisible();
});
test('should zoom in when clicking zoom in button', async ({ page }) => {
await waitForMap(page);
const getZoom = () => page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.map?.getZoom() || null;
});
const initialZoom = await getZoom();
await page.locator('.leaflet-control-zoom-in').click();
await page.waitForTimeout(500);
const newZoom = await getZoom();
expect(newZoom).toBeGreaterThan(initialZoom);
});
test('should zoom out when clicking zoom out button', async ({ page }) => {
await waitForMap(page);
const getZoom = () => page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.map?.getZoom() || null;
});
const initialZoom = await getZoom();
await page.locator('.leaflet-control-zoom-out').click();
await page.waitForTimeout(500);
const newZoom = await getZoom();
expect(newZoom).toBeLessThan(initialZoom);
});
test('should switch between map tile layers', async ({ page }) => {
await waitForMap(page);
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const getSelectedLayer = () => page.evaluate(() => {
const radio = document.querySelector('.leaflet-control-layers-base input[type="radio"]:checked');
return radio ? radio.nextSibling.textContent.trim() : null;
});
const initialLayer = await getSelectedLayer();
await page.locator('.leaflet-control-layers-base input[type="radio"]:not(:checked)').first().click();
await page.waitForTimeout(500);
const newLayer = await getSelectedLayer();
expect(newLayer).not.toBe(initialLayer);
});
test('should navigate to specific date and display points layer', async ({ page }) => {
// Wait for map to be ready
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
// Navigate to date 13.10.2024
// First, need to expand the date controls on mobile (if collapsed)
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
// Clear and fill in the start date/time input (midnight)
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
// Clear and fill in the end date/time input (end of day)
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
// Click the Search button to submit
await page.click('input[type="submit"][value="Search"]');
// Wait for page navigation and map reload
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000); // Wait for map to reinitialize
// Close onboarding modal if it appears after navigation
const onboardingModal = page.locator('#getting_started');
const isModalOpen = await onboardingModal.evaluate((dialog) => dialog.open).catch(() => false);
if (isModalOpen) {
await page.locator('#getting_started button.btn-primary').click();
await page.waitForTimeout(500);
}
// Open layer control to enable points
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
// Enable points layer if not already enabled
const pointsCheckbox = page.locator('.leaflet-control-layers-overlays input[type="checkbox"]').first();
const isChecked = await pointsCheckbox.isChecked();
if (!isChecked) {
await pointsCheckbox.check();
await page.waitForTimeout(1000); // Wait for points to render
}
// Verify points are visible on the map
const layerInfo = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (!controller) {
return { error: 'Controller not found' };
}
const result = {
hasMarkersLayer: !!controller.markersLayer,
markersCount: 0,
hasPolylinesLayer: !!controller.polylinesLayer,
polylinesCount: 0,
hasTracksLayer: !!controller.tracksLayer,
tracksCount: 0,
};
// Check markers layer
if (controller.markersLayer && controller.markersLayer._layers) {
result.markersCount = Object.keys(controller.markersLayer._layers).length;
}
// Check polylines layer
if (controller.polylinesLayer && controller.polylinesLayer._layers) {
result.polylinesCount = Object.keys(controller.polylinesLayer._layers).length;
}
// Check tracks layer
if (controller.tracksLayer && controller.tracksLayer._layers) {
result.tracksCount = Object.keys(controller.tracksLayer._layers).length;
}
return result;
});
// Verify that at least one layer has data
const hasData = layerInfo.markersCount > 0 ||
layerInfo.polylinesCount > 0 ||
layerInfo.tracksCount > 0;
expect(hasData).toBe(true);
});
test('should enable Routes layer and display routes', async ({ page }) => {
// Wait for map to be ready
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
// Navigate to date with data
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
await page.click('input[type="submit"][value="Search"]');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Close onboarding modal if present
const onboardingModal = page.locator('#getting_started');
const isModalOpen = await onboardingModal.evaluate((dialog) => dialog.open).catch(() => false);
if (isModalOpen) {
await page.locator('#getting_started button.btn-primary').click();
await page.waitForTimeout(500);
}
// Open layer control and enable Routes
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const routesCheckbox = page.locator('.leaflet-control-layers-overlays label:has-text("Routes") input[type="checkbox"]');
const isChecked = await routesCheckbox.isChecked();
if (!isChecked) {
await routesCheckbox.check();
await page.waitForTimeout(1000);
}
// Verify routes are visible
const hasRoutes = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.polylinesLayer && controller.polylinesLayer._layers) {
return Object.keys(controller.polylinesLayer._layers).length > 0;
}
return false;
});
expect(hasRoutes).toBe(true);
});
test('should enable Heatmap layer and display heatmap', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Heatmap');
const hasHeatmap = await page.locator('.leaflet-heatmap-layer').isVisible();
expect(hasHeatmap).toBe(true);
});
test('should enable Fog of War layer and display fog', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Fog of War');
const hasFog = await page.evaluate(() => {
const fogCanvas = document.getElementById('fog');
return fogCanvas && fogCanvas instanceof HTMLCanvasElement;
});
expect(hasFog).toBe(true);
});
test('should enable Areas layer and display areas', async ({ page }) => {
await waitForMap(page);
const hasAreasLayer = await page.evaluate(() => {
const mapElement = document.querySelector('#map');
const app = window.Stimulus;
const controller = app?.getControllerForElementAndIdentifier(mapElement, 'maps');
return controller?.areasLayer !== null && controller?.areasLayer !== undefined;
});
expect(hasAreasLayer).toBe(true);
});
test('should enable Suggested Visits layer', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Suggested Visits');
const hasSuggestedVisits = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.visitCircles !== null &&
controller?.visitsManager?.visitCircles !== undefined;
});
expect(hasSuggestedVisits).toBe(true);
});
test('should enable Confirmed Visits layer', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Confirmed Visits');
const hasConfirmedVisits = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.confirmedVisitCircles !== null &&
controller?.visitsManager?.confirmedVisitCircles !== undefined;
});
expect(hasConfirmedVisits).toBe(true);
});
test('should enable Scratch Map layer and display visited countries', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Scratch Map');
// Wait a bit for the layer to load country borders
await page.waitForTimeout(2000);
// Verify scratch layer exists and has been initialized
const hasScratchLayer = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
// Check if scratchLayerManager exists
if (!controller?.scratchLayerManager) return false;
// Check if scratch layer was created
const scratchLayer = controller.scratchLayerManager.getLayer();
return scratchLayer !== null && scratchLayer !== undefined;
});
expect(hasScratchLayer).toBe(true);
});
test('should remember enabled layers across page reloads', async ({ page }) => {
await waitForMap(page);
// Enable multiple layers
await enableLayer(page, 'Points');
await enableLayer(page, 'Routes');
await enableLayer(page, 'Heatmap');
await page.waitForTimeout(500);
// Get current layer states
const getLayerStates = () => page.evaluate(() => {
const layers = {};
document.querySelectorAll('.leaflet-control-layers-overlays input[type="checkbox"]').forEach(checkbox => {
const label = checkbox.parentElement.textContent.trim();
layers[label] = checkbox.checked;
});
return layers;
});
const layersBeforeReload = await getLayerStates();
// Reload the page
await page.reload();
await closeOnboardingModal(page);
await waitForMap(page);
await page.waitForTimeout(1000); // Wait for layers to restore
// Get layer states after reload
const layersAfterReload = await getLayerStates();
// Verify Points, Routes, and Heatmap are still enabled
expect(layersAfterReload['Points']).toBe(true);
expect(layersAfterReload['Routes']).toBe(true);
expect(layersAfterReload['Heatmap']).toBe(true);
// Verify layer states match before and after
expect(layersAfterReload).toEqual(layersBeforeReload);
});
test.describe('Point Interactions', () => {
test.beforeEach(async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Points');
await page.waitForTimeout(1500);
// Pan map to ensure a marker is in viewport
await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.markers && controller.markers.length > 0) {
const firstMarker = controller.markers[0];
controller.map.setView([firstMarker[0], firstMarker[1]], 14);
}
});
await page.waitForTimeout(1000);
});
test('should have draggable markers on the map', async ({ page }) => {
// Verify markers have draggable class
const marker = page.locator('.leaflet-marker-icon').first();
await expect(marker).toBeVisible();
// Check if marker has draggable class
const isDraggable = await marker.evaluate((el) => {
return el.classList.contains('leaflet-marker-draggable');
});
expect(isDraggable).toBe(true);
// Verify marker position can be retrieved (required for drag operations)
const box = await marker.boundingBox();
expect(box).not.toBeNull();
expect(box.x).toBeGreaterThan(0);
expect(box.y).toBeGreaterThan(0);
});
test('should open popup when clicking a point', async ({ page }) => {
// Click on a marker with force to ensure interaction
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible();
});
test('should display correct popup content with point data', async ({ page }) => {
// Click on a marker
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Get popup content
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent).toBeVisible();
const content = await popupContent.textContent();
// Verify all required fields are present
expect(content).toContain('Timestamp:');
expect(content).toContain('Latitude:');
expect(content).toContain('Longitude:');
expect(content).toContain('Altitude:');
expect(content).toContain('Speed:');
expect(content).toContain('Battery:');
expect(content).toContain('Id:');
});
test('should delete a point and redraw route', async ({ page }) => {
// Enable Routes layer to verify route redraw
await enableLayer(page, 'Routes');
await page.waitForTimeout(1000);
// Count initial markers and get point ID
const initialData = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
const markerCount = controller?.markersLayer ? Object.keys(controller.markersLayer._layers).length : 0;
const polylineCount = controller?.polylinesLayer ? Object.keys(controller.polylinesLayer._layers).length : 0;
return { markerCount, polylineCount };
});
// Click on a marker to open popup
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Verify popup opened
await expect(page.locator('.leaflet-popup')).toBeVisible();
// Get the point ID from popup before deleting
const pointId = await page.locator('.leaflet-popup-content').evaluate((content) => {
const match = content.textContent.match(/Id:\s*(\d+)/);
return match ? match[1] : null;
});
expect(pointId).not.toBeNull();
// Find delete button (might be a link or button with "Delete" text)
const deleteButton = page.locator('.leaflet-popup-content a:has-text("Delete"), .leaflet-popup-content button:has-text("Delete")').first();
const hasDeleteButton = await deleteButton.count() > 0;
if (hasDeleteButton) {
// Handle confirmation dialog
page.once('dialog', dialog => {
expect(dialog.message()).toContain('delete');
dialog.accept();
});
await deleteButton.click();
await page.waitForTimeout(2000); // Wait for deletion to complete
// Verify marker count decreased
const finalData = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
const markerCount = controller?.markersLayer ? Object.keys(controller.markersLayer._layers).length : 0;
const polylineCount = controller?.polylinesLayer ? Object.keys(controller.polylinesLayer._layers).length : 0;
return { markerCount, polylineCount };
});
// Verify at least one marker was removed
expect(finalData.markerCount).toBeLessThan(initialData.markerCount);
// Verify routes still exist (they should be redrawn)
expect(finalData.polylineCount).toBeGreaterThanOrEqual(0);
// Verify success flash message appears (optional - may take time to render)
const flashMessage = page.locator('#flash-messages [role="alert"]').filter({ hasText: /deleted successfully/i });
const flashVisible = await flashMessage.isVisible({ timeout: 5000 }).catch(() => false);
if (flashVisible) {
console.log('✓ Flash message "Point deleted successfully" is visible');
} else {
console.log('⚠ Flash message not detected (this is acceptable if deletion succeeded)');
}
} else {
// If no delete button, just verify the test setup worked
console.log('No delete button found in popup - this might be expected based on permissions');
}
});
});
test.describe('Visit Interactions', () => {
test.beforeEach(async ({ page }) => {
await waitForMap(page);
// Navigate to a date range that includes visits (last month to now)
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
// Set date range to last month
await page.click('a:has-text("Last month")');
await page.waitForTimeout(2000);
await closeOnboardingModal(page);
await waitForMap(page);
await enableLayer(page, 'Confirmed Visits');
await page.waitForTimeout(2000);
// Pan map to ensure a visit marker is in viewport
await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit && firstVisit._latlng) {
controller.map.setView(firstVisit._latlng, 14);
}
}
});
await page.waitForTimeout(1000);
});
test('should click on a confirmed visit and open popup', async ({ page }) => {
// Debug: Check what visit circles exist
const allCircles = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
return {
count: Object.keys(layers).length,
hasLayers: Object.keys(layers).length > 0
};
}
return { count: 0, hasLayers: false };
});
console.log('Confirmed visits in layer:', allCircles);
// If we have visits in the layer but can't find DOM elements, use coordinates
if (!allCircles.hasLayers) {
console.log('No confirmed visits found - skipping test');
return;
}
// Click on the visit using map coordinates
const visitClicked = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit && firstVisit._latlng) {
// Trigger click event on the visit
firstVisit.fire('click');
return true;
}
}
return false;
});
if (!visitClicked) {
console.log('Could not click visit - skipping test');
return;
}
await page.waitForTimeout(500);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible();
});
test('should display correct content in confirmed visit popup', async ({ page }) => {
// Click visit programmatically
const visitClicked = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit) {
firstVisit.fire('click');
return true;
}
}
return false;
});
if (!visitClicked) {
console.log('No confirmed visits found - skipping test');
return;
}
await page.waitForTimeout(500);
// Get popup content
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent).toBeVisible();
const content = await popupContent.textContent();
// Verify visit information is present
expect(content).toMatch(/Visit|Place|Duration|Started|Ended/i);
});
test('should change place in dropdown and save', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Look for place dropdown/select in popup
const placeSelect = page.locator('.leaflet-popup-content select, .leaflet-popup-content [role="combobox"]').first();
const hasPlaceDropdown = await placeSelect.count() > 0;
if (!hasPlaceDropdown) {
console.log('No place dropdown found - skipping test');
return;
}
// Get current value
const initialValue = await placeSelect.inputValue().catch(() => null);
// Select a different option
await placeSelect.selectOption({ index: 1 });
await page.waitForTimeout(300);
// Find and click save button
const saveButton = page.locator('.leaflet-popup-content button:has-text("Save"), .leaflet-popup-content input[type="submit"]').first();
const hasSaveButton = await saveButton.count() > 0;
if (hasSaveButton) {
await saveButton.click();
await page.waitForTimeout(1000);
// Verify success message or popup closes
const popupStillVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
// Either popup closes or stays open with updated content
expect(popupStillVisible === false || popupStillVisible === true).toBe(true);
}
});
test('should change visit name and save', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Look for name input field
const nameInput = page.locator('.leaflet-popup-content input[type="text"]').first();
const hasNameInput = await nameInput.count() > 0;
if (!hasNameInput) {
console.log('No name input found - skipping test');
return;
}
// Change the name
const newName = `Test Visit ${Date.now()}`;
await nameInput.fill(newName);
await page.waitForTimeout(300);
// Find and click save button
const saveButton = page.locator('.leaflet-popup-content button:has-text("Save"), .leaflet-popup-content input[type="submit"]').first();
const hasSaveButton = await saveButton.count() > 0;
if (hasSaveButton) {
await saveButton.click();
await page.waitForTimeout(1000);
// Verify flash message or popup closes
const flashOrClose = await page.locator('#flash-messages [role="alert"]').isVisible({ timeout: 2000 }).catch(() => false);
expect(flashOrClose === true || flashOrClose === false).toBe(true);
}
});
test('should delete confirmed visit from map', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
// Count initial visits
const initialVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Find delete button
const deleteButton = page.locator('.leaflet-popup-content button:has-text("Delete"), .leaflet-popup-content a:has-text("Delete")').first();
const hasDeleteButton = await deleteButton.count() > 0;
if (!hasDeleteButton) {
console.log('No delete button found - skipping test');
return;
}
// Handle confirmation dialog
page.once('dialog', dialog => {
expect(dialog.message()).toMatch(/delete|remove/i);
dialog.accept();
});
await deleteButton.click();
await page.waitForTimeout(2000);
// Verify visit count decreased
const finalVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
expect(finalVisitCount).toBeLessThan(initialVisitCount);
});
});
});

View file

@ -0,0 +1,260 @@
import { test, expect } from '@playwright/test';
import { navigateToMap } from '../helpers/navigation.js';
import { waitForMap } from '../helpers/map.js';
/**
* Helper to wait for add visit controller to be fully initialized
*/
async function waitForAddVisitController(page) {
await page.waitForTimeout(2000); // Wait for controller to connect and attach handlers
}
test.describe('Add Visit Control', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
await waitForAddVisitController(page);
});
test('should show add visit button control', async ({ page }) => {
const addVisitButton = page.locator('.add-visit-button');
await expect(addVisitButton).toBeVisible();
});
test('should enable add visit mode when clicked', async ({ page }) => {
const addVisitButton = page.locator('.add-visit-button');
await addVisitButton.click();
await page.waitForTimeout(1000);
// Verify flash message appears
const flashMessage = page.locator('#flash-messages [role="alert"]:has-text("Click on the map")');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// Verify cursor changed to crosshair
const cursor = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container?.style.cursor;
});
expect(cursor).toBe('crosshair');
// Verify button has active state (background color applied)
const hasActiveStyle = await addVisitButton.evaluate((el) => {
return el.style.backgroundColor !== '';
});
expect(hasActiveStyle).toBe(true);
});
test('should open popup form when map is clicked', async ({ page }) => {
const addVisitButton = page.locator('.add-visit-button');
await addVisitButton.click();
await page.waitForTimeout(500);
// Click on map - use bottom left corner which is less likely to have points
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
await page.mouse.click(bbox.x + bbox.width * 0.2, bbox.y + bbox.height * 0.8);
await page.waitForTimeout(1000);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible({ timeout: 10000 });
// Verify popup contains the add visit form
await expect(popup.locator('h3:has-text("Add New Visit")')).toBeVisible();
// Verify marker appears (📍 emoji with class add-visit-marker)
const marker = page.locator('.add-visit-marker');
await expect(marker).toBeVisible();
});
test('should display correct form content in popup', async ({ page }) => {
// Enable mode and click map
await page.locator('.add-visit-button').click();
await page.waitForTimeout(500);
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
await page.mouse.click(bbox.x + bbox.width * 0.2, bbox.y + bbox.height * 0.8);
await page.waitForTimeout(1000);
// Verify popup content has all required elements
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent.locator('h3:has-text("Add New Visit")')).toBeVisible();
await expect(popupContent.locator('input#visit-name')).toBeVisible();
await expect(popupContent.locator('input#visit-start')).toBeVisible();
await expect(popupContent.locator('input#visit-end')).toBeVisible();
await expect(popupContent.locator('button:has-text("Create Visit")')).toBeVisible();
await expect(popupContent.locator('button:has-text("Cancel")')).toBeVisible();
// Verify name field has focus
const nameFieldFocused = await page.evaluate(() => {
return document.activeElement?.id === 'visit-name';
});
expect(nameFieldFocused).toBe(true);
// Verify start and end time have default values
const startValue = await page.locator('input#visit-start').inputValue();
const endValue = await page.locator('input#visit-end').inputValue();
expect(startValue).toBeTruthy();
expect(endValue).toBeTruthy();
});
test('should hide popup and remove marker when cancel is clicked', async ({ page }) => {
// Enable mode and click map
await page.locator('.add-visit-button').click();
await page.waitForTimeout(500);
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
await page.mouse.click(bbox.x + bbox.width * 0.2, bbox.y + bbox.height * 0.8);
await page.waitForTimeout(1000);
// Verify popup and marker exist
await expect(page.locator('.leaflet-popup')).toBeVisible();
await expect(page.locator('.add-visit-marker')).toBeVisible();
// Click cancel button
await page.locator('#cancel-visit').click();
await page.waitForTimeout(500);
// Verify popup is hidden
const popupVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
expect(popupVisible).toBe(false);
// Verify marker is removed
const markerCount = await page.locator('.add-visit-marker').count();
expect(markerCount).toBe(0);
// Verify cursor is reset to default
const cursor = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container?.style.cursor;
});
expect(cursor).toBe('');
// Verify mode was exited (cursor should be reset)
const cursorReset = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container?.style.cursor === '';
});
expect(cursorReset).toBe(true);
});
test('should create visit and show marker on map when submitted', async ({ page }) => {
// Get initial confirmed visit count
const initialCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
// Enable mode and click map
await page.locator('.add-visit-button').click();
await page.waitForTimeout(500);
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
await page.mouse.click(bbox.x + bbox.width * 0.2, bbox.y + bbox.height * 0.8);
await page.waitForTimeout(1000);
// Fill form with unique visit name
const visitName = `E2E Test Visit ${Date.now()}`;
await page.locator('#visit-name').fill(visitName);
// Submit form
await page.locator('button:has-text("Create Visit")').click();
await page.waitForTimeout(2000);
// Verify success message
const flashMessage = page.locator('#flash-messages [role="alert"]:has-text("created successfully")');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// Verify popup is closed
const popupVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
expect(popupVisible).toBe(false);
// Verify confirmed visit marker count increased
const finalCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
expect(finalCount).toBeGreaterThan(initialCount);
});
test('should disable add visit mode when clicked second time', async ({ page }) => {
const addVisitButton = page.locator('.add-visit-button');
// First click - enable mode
await addVisitButton.click();
await page.waitForTimeout(500);
// Verify mode is enabled
const cursorEnabled = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container?.style.cursor === 'crosshair';
});
expect(cursorEnabled).toBe(true);
// Second click - disable mode
await addVisitButton.click();
await page.waitForTimeout(500);
// Verify cursor is reset
const cursorDisabled = await page.evaluate(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container?.style.cursor;
});
expect(cursorDisabled).toBe('');
// Verify mode was exited by checking if we can click map without creating marker
const isAddingVisit = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'add-visit');
return controller?.isAddingVisit === true;
});
expect(isAddingVisit).toBe(false);
});
test('should ensure only one visit popup is open at a time', async ({ page }) => {
const addVisitButton = page.locator('.add-visit-button');
await addVisitButton.click();
await page.waitForTimeout(500);
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
// Click first location on map
await page.mouse.click(bbox.x + bbox.width * 0.3, bbox.y + bbox.height * 0.3);
await page.waitForTimeout(500);
// Verify first popup exists
let popupCount = await page.locator('.leaflet-popup').count();
expect(popupCount).toBe(1);
// Get the content of first popup to verify it exists
const firstPopupContent = await page.locator('.leaflet-popup-content input#visit-name').count();
expect(firstPopupContent).toBe(1);
// Click second location on map
await page.mouse.click(bbox.x + bbox.width * 0.7, bbox.y + bbox.height * 0.7);
await page.waitForTimeout(500);
// Verify still only one popup exists (old one was closed, new one opened)
popupCount = await page.locator('.leaflet-popup').count();
expect(popupCount).toBe(1);
// Verify the popup contains the add visit form (not some other popup)
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent.locator('h3:has-text("Add New Visit")')).toBeVisible();
await expect(popupContent.locator('input#visit-name')).toBeVisible();
// Verify only one marker exists
const markerCount = await page.locator('.add-visit-marker').count();
expect(markerCount).toBe(1);
});
});

View file

@ -1,37 +1,7 @@
const { test, expect } = require('@playwright/test');
// Helper function to draw selection rectangle and wait for delete button
async function drawSelectionRectangle(page) {
// Click area selection tool
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
// Draw a rectangle on the map to select points
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
// Draw rectangle covering most of the map to ensure we select points
const startX = bbox.x + bbox.width * 0.2;
const startY = bbox.y + bbox.height * 0.2;
const endX = bbox.x + bbox.width * 0.8;
const endY = bbox.y + bbox.height * 0.8;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY, { steps: 10 }); // Add steps for smoother drag
await page.mouse.up();
// Wait longer for API calls and drawer animations
await page.waitForTimeout(2000);
// Wait for drawer to open (it should open automatically after selection)
await page.waitForSelector('#visits-drawer.open', { timeout: 15000 });
// Wait for delete button to appear in the drawer (indicates selection is complete)
await page.waitForSelector('#delete-selection-button', { timeout: 15000 });
await page.waitForTimeout(500); // Brief wait for UI to stabilize
}
import { test, expect } from '@playwright/test';
import { drawSelectionRectangle } from '../helpers/selection.js';
import { navigateToDate, closeOnboardingModal } from '../helpers/navigation.js';
import { waitForMap, enableLayer } from '../helpers/map.js';
test.describe('Bulk Delete Points', () => {
test.beforeEach(async ({ page }) => {
@ -42,45 +12,16 @@ test.describe('Bulk Delete Points', () => {
});
// Wait for map to be initialized
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
await waitForMap(page);
// Close onboarding modal if present
const onboardingModal = page.locator('#getting_started');
const isModalOpen = await onboardingModal.evaluate((dialog) => dialog.open).catch(() => false);
if (isModalOpen) {
await page.locator('#getting_started button.btn-primary').click();
await page.waitForTimeout(500);
}
await closeOnboardingModal(page);
// Navigate to a date with points (October 13, 2024)
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
// Click the Search button to submit
await page.click('input[type="submit"][value="Search"]');
// Wait for page navigation and map reload
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await navigateToDate(page, '2024-10-13T00:00', '2024-10-13T23:59');
// Enable Points layer
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const pointsCheckbox = page.locator('.leaflet-control-layers-overlays label:has-text("Points") input[type="checkbox"]');
const isChecked = await pointsCheckbox.isChecked();
if (!isChecked) {
await pointsCheckbox.check();
await page.waitForTimeout(1000);
}
await enableLayer(page, 'Points');
});
test('should show area selection tool button', async ({ page }) => {

View file

@ -0,0 +1,308 @@
import { test, expect } from '@playwright/test';
import { closeOnboardingModal } from '../helpers/navigation.js';
/**
* Calendar Panel Tests
*
* Tests for the calendar panel control that allows users to navigate between
* different years and months. The panel is opened via the "Toggle Panel" button
* in the top-right corner of the map.
*/
test.describe('Calendar Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/map');
await closeOnboardingModal(page);
// Wait for map to be fully loaded
await page.waitForSelector('.leaflet-container', { state: 'visible', timeout: 10000 });
await page.waitForTimeout(2000); // Wait for all controls to be initialized
});
/**
* Helper function to find and click the calendar toggle button
*/
async function clickCalendarButton(page) {
// The calendar button is the "Toggle Panel" button with a calendar icon
// It's the third button in the top-right control stack (after Select Area and Add Visit)
const calendarButton = await page.locator('button.toggle-panel-button').first();
await expect(calendarButton).toBeVisible({ timeout: 5000 });
await calendarButton.click();
await page.waitForTimeout(500); // Wait for panel animation
}
/**
* Helper function to check if panel is visible
*/
async function isPanelVisible(page) {
const panel = page.locator('.leaflet-right-panel');
const isVisible = await panel.isVisible().catch(() => false);
if (!isVisible) return false;
const displayStyle = await panel.evaluate(el => el.style.display);
return displayStyle !== 'none';
}
test('should open calendar panel on first click', async ({ page }) => {
// Verify panel is not visible initially
const initiallyVisible = await isPanelVisible(page);
expect(initiallyVisible).toBe(false);
// Click calendar button
await clickCalendarButton(page);
// Verify panel is now visible
const panelVisible = await isPanelVisible(page);
expect(panelVisible).toBe(true);
// Verify panel contains expected elements
const yearSelect = page.locator('#year-select');
await expect(yearSelect).toBeVisible();
const monthsGrid = page.locator('#months-grid');
await expect(monthsGrid).toBeVisible();
// Verify "Whole year" link is present
const wholeYearLink = page.locator('#whole-year-link');
await expect(wholeYearLink).toBeVisible();
});
test('should close calendar panel on second click', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
await page.waitForTimeout(300);
// Verify panel is visible
let panelVisible = await isPanelVisible(page);
expect(panelVisible).toBe(true);
// Click button again to close
await clickCalendarButton(page);
await page.waitForTimeout(300);
// Verify panel is hidden
panelVisible = await isPanelVisible(page);
expect(panelVisible).toBe(false);
});
test('should allow year selection', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
// Wait for year select to be populated (it loads from API)
await page.waitForTimeout(2000);
const yearSelect = page.locator('#year-select');
await expect(yearSelect).toBeVisible();
// Get available years
const options = await yearSelect.locator('option:not([disabled])').all();
// Should have at least one year available
expect(options.length).toBeGreaterThan(0);
// Select the first available year
const firstYearOption = options[0];
const yearValue = await firstYearOption.getAttribute('value');
await yearSelect.selectOption(yearValue);
// Verify year was selected
const selectedValue = await yearSelect.inputValue();
expect(selectedValue).toBe(yearValue);
});
test('should navigate to month when clicking month button', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
// Wait for months to load
await page.waitForTimeout(3000);
// Select year 2024 (which has October data in demo)
const yearSelect = page.locator('#year-select');
await yearSelect.selectOption('2024');
await page.waitForTimeout(500);
// Find October button (demo data has October 2024)
const octoberButton = page.locator('#months-grid a[data-month-name="Oct"]');
await expect(octoberButton).toBeVisible({ timeout: 5000 });
// Verify October is enabled (not disabled)
const isDisabled = await octoberButton.evaluate(el => el.classList.contains('disabled'));
expect(isDisabled).toBe(false);
// Verify button is clickable
const pointerEvents = await octoberButton.evaluate(el => el.style.pointerEvents);
expect(pointerEvents).not.toBe('none');
// Get the expected href before clicking
const expectedHref = await octoberButton.getAttribute('href');
expect(expectedHref).toBeTruthy();
const decodedHref = decodeURIComponent(expectedHref);
expect(decodedHref).toContain('map?');
expect(decodedHref).toContain('start_at=2024-10-01T00:00');
expect(decodedHref).toContain('end_at=2024-10-31T23:59');
// Click the month button and wait for navigation
await Promise.all([
page.waitForURL('**/map**', { timeout: 10000 }),
octoberButton.click()
]);
// Wait for page to settle
await page.waitForLoadState('networkidle', { timeout: 10000 });
// Verify we navigated to the map page
expect(page.url()).toContain('/map');
// Verify map loaded with data
await page.waitForSelector('.leaflet-container', { state: 'visible', timeout: 10000 });
});
test('should navigate to whole year when clicking "Whole year" button', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
// Wait for panel to load
await page.waitForTimeout(2000);
const wholeYearLink = page.locator('#whole-year-link');
await expect(wholeYearLink).toBeVisible();
// Get the href and decode it
const href = await wholeYearLink.getAttribute('href');
expect(href).toBeTruthy();
const decodedHref = decodeURIComponent(href);
expect(decodedHref).toContain('map?');
expect(decodedHref).toContain('start_at=');
expect(decodedHref).toContain('end_at=');
// Href should contain full year dates (01-01 to 12-31)
expect(decodedHref).toContain('-01-01T00:00');
expect(decodedHref).toContain('-12-31T23:59');
// Store the expected year from the href
const yearMatch = decodedHref.match(/(\d{4})-01-01/);
expect(yearMatch).toBeTruthy();
const expectedYear = yearMatch[1];
// Click the link and wait for navigation
await Promise.all([
page.waitForURL('**/map**', { timeout: 10000 }),
wholeYearLink.click()
]);
// Wait for page to settle
await page.waitForLoadState('networkidle', { timeout: 10000 });
// Verify we navigated to the map page
expect(page.url()).toContain('/map');
// The URL parameters might be processed differently (e.g., stripped by Turbo or redirected)
// Instead of checking URL, verify the panel updates to show the whole year is selected
// by checking the year in the select dropdown
const panelVisible = await isPanelVisible(page);
if (!panelVisible) {
// Panel might have closed on navigation, reopen it
await clickCalendarButton(page);
await page.waitForTimeout(1000);
}
const yearSelect = page.locator('#year-select');
const selectedYear = await yearSelect.inputValue();
expect(selectedYear).toBe(expectedYear);
});
test('should update month buttons when year is changed', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
// Wait for data to load
await page.waitForTimeout(2000);
const yearSelect = page.locator('#year-select');
// Get available years
const options = await yearSelect.locator('option:not([disabled])').all();
if (options.length < 2) {
console.log('Test skipped: Less than 2 years available');
test.skip();
return;
}
// Select first year and capture month states
const firstYearOption = options[0];
const firstYear = await firstYearOption.getAttribute('value');
await yearSelect.selectOption(firstYear);
await page.waitForTimeout(500);
// Get enabled months for first year
const firstYearMonths = await page.locator('#months-grid a:not(.disabled)').count();
// Select second year
const secondYearOption = options[1];
const secondYear = await secondYearOption.getAttribute('value');
await yearSelect.selectOption(secondYear);
await page.waitForTimeout(500);
// Get enabled months for second year
const secondYearMonths = await page.locator('#months-grid a:not(.disabled)').count();
// Months should be different (unless both years have same tracked months)
// At minimum, verify that month buttons are updated (content changed from loading dots)
const monthButtons = await page.locator('#months-grid a').all();
for (const button of monthButtons) {
const buttonText = await button.textContent();
// Should not contain loading dots anymore
expect(buttonText).not.toContain('loading');
}
});
test('should highlight active month based on current URL parameters', async ({ page }) => {
// Navigate to a specific month first
await page.goto('/map?start_at=2024-10-01T00:00&end_at=2024-10-31T23:59');
await closeOnboardingModal(page);
await page.waitForSelector('.leaflet-container', { state: 'visible', timeout: 10000 });
await page.waitForTimeout(2000);
// Open calendar panel
await clickCalendarButton(page);
await page.waitForTimeout(2000);
// Find October button (month index 9, displayed as "Oct")
const octoberButton = page.locator('#months-grid a[data-month-name="Oct"]');
await expect(octoberButton).toBeVisible();
// Verify October is marked as active
const hasActiveClass = await octoberButton.evaluate(el =>
el.classList.contains('btn-active')
);
expect(hasActiveClass).toBe(true);
});
test('should show visited cities section in panel', async ({ page }) => {
// Open panel
await clickCalendarButton(page);
await page.waitForTimeout(2000);
// Verify visited cities section is present
const visitedCitiesContainer = page.locator('#visited-cities-container');
await expect(visitedCitiesContainer).toBeVisible();
const visitedCitiesTitle = visitedCitiesContainer.locator('h3');
await expect(visitedCitiesTitle).toHaveText('Visited cities');
const visitedCitiesList = page.locator('#visited-cities-list');
await expect(visitedCitiesList).toBeVisible();
// List should eventually load (either with cities or "No places visited")
await page.waitForTimeout(2000);
const listContent = await visitedCitiesList.textContent();
expect(listContent.length).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,157 @@
import { test, expect } from '@playwright/test';
import { navigateToMap, closeOnboardingModal, navigateToDate } from '../helpers/navigation.js';
import { waitForMap, getMapZoom } from '../helpers/map.js';
test.describe('Map Page', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
});
test('should load map container and display map with controls', async ({ page }) => {
await expect(page.locator('#map')).toBeVisible();
await waitForMap(page);
// Verify zoom controls are present
await expect(page.locator('.leaflet-control-zoom')).toBeVisible();
// Verify custom map controls are present (from map_controls.js)
await expect(page.locator('.add-visit-button')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.toggle-panel-button')).toBeVisible();
await expect(page.locator('.drawer-button')).toBeVisible();
await expect(page.locator('#selection-tool-button')).toBeVisible();
});
test('should zoom in when clicking zoom in button', async ({ page }) => {
await waitForMap(page);
const initialZoom = await getMapZoom(page);
await page.locator('.leaflet-control-zoom-in').click();
await page.waitForTimeout(500);
const newZoom = await getMapZoom(page);
expect(newZoom).toBeGreaterThan(initialZoom);
});
test('should zoom out when clicking zoom out button', async ({ page }) => {
await waitForMap(page);
const initialZoom = await getMapZoom(page);
await page.locator('.leaflet-control-zoom-out').click();
await page.waitForTimeout(500);
const newZoom = await getMapZoom(page);
expect(newZoom).toBeLessThan(initialZoom);
});
test('should switch between map tile layers', async ({ page }) => {
await waitForMap(page);
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const getSelectedLayer = () => page.evaluate(() => {
const radio = document.querySelector('.leaflet-control-layers-base input[type="radio"]:checked');
return radio ? radio.nextSibling.textContent.trim() : null;
});
const initialLayer = await getSelectedLayer();
await page.locator('.leaflet-control-layers-base input[type="radio"]:not(:checked)').first().click();
await page.waitForTimeout(500);
const newLayer = await getSelectedLayer();
expect(newLayer).not.toBe(initialLayer);
});
test('should navigate to specific date and display points layer', async ({ page }) => {
// Wait for map to be ready
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
// Navigate to date 13.10.2024
// First, need to expand the date controls on mobile (if collapsed)
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
// Clear and fill in the start date/time input (midnight)
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
// Clear and fill in the end date/time input (end of day)
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
// Click the Search button to submit
await page.click('input[type="submit"][value="Search"]');
// Wait for page navigation and map reload
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000); // Wait for map to reinitialize
// Close onboarding modal if it appears after navigation
await closeOnboardingModal(page);
// Open layer control to enable points
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
// Enable points layer if not already enabled
const pointsCheckbox = page.locator('.leaflet-control-layers-overlays input[type="checkbox"]').first();
const isChecked = await pointsCheckbox.isChecked();
if (!isChecked) {
await pointsCheckbox.check();
await page.waitForTimeout(1000); // Wait for points to render
}
// Verify points are visible on the map
const layerInfo = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (!controller) {
return { error: 'Controller not found' };
}
const result = {
hasMarkersLayer: !!controller.markersLayer,
markersCount: 0,
hasPolylinesLayer: !!controller.polylinesLayer,
polylinesCount: 0,
hasTracksLayer: !!controller.tracksLayer,
tracksCount: 0,
};
// Check markers layer
if (controller.markersLayer && controller.markersLayer._layers) {
result.markersCount = Object.keys(controller.markersLayer._layers).length;
}
// Check polylines layer
if (controller.polylinesLayer && controller.polylinesLayer._layers) {
result.polylinesCount = Object.keys(controller.polylinesLayer._layers).length;
}
// Check tracks layer
if (controller.tracksLayer && controller.tracksLayer._layers) {
result.tracksCount = Object.keys(controller.tracksLayer._layers).length;
}
return result;
});
// Verify that at least one layer has data
const hasData = layerInfo.markersCount > 0 ||
layerInfo.polylinesCount > 0 ||
layerInfo.tracksCount > 0;
expect(hasData).toBe(true);
});
});

184
e2e/map/map-layers.spec.js Normal file
View file

@ -0,0 +1,184 @@
import { test, expect } from '@playwright/test';
import { navigateToMap, closeOnboardingModal } from '../helpers/navigation.js';
import { waitForMap, enableLayer } from '../helpers/map.js';
test.describe('Map Layers', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
});
test('should enable Routes layer and display routes', async ({ page }) => {
// Wait for map to be ready
await page.waitForFunction(() => {
const container = document.querySelector('#map [data-maps-target="container"]');
return container && container._leaflet_id !== undefined;
}, { timeout: 10000 });
// Navigate to date with data
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
await page.click('input[type="submit"][value="Search"]');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Close onboarding modal if present
await closeOnboardingModal(page);
// Open layer control and enable Routes
await page.locator('.leaflet-control-layers').hover();
await page.waitForTimeout(300);
const routesCheckbox = page.locator('.leaflet-control-layers-overlays label:has-text("Routes") input[type="checkbox"]');
const isChecked = await routesCheckbox.isChecked();
if (!isChecked) {
await routesCheckbox.check();
await page.waitForTimeout(1000);
}
// Verify routes are visible
const hasRoutes = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.polylinesLayer && controller.polylinesLayer._layers) {
return Object.keys(controller.polylinesLayer._layers).length > 0;
}
return false;
});
expect(hasRoutes).toBe(true);
});
test('should enable Heatmap layer and display heatmap', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Heatmap');
const hasHeatmap = await page.locator('.leaflet-heatmap-layer').isVisible();
expect(hasHeatmap).toBe(true);
});
test('should enable Fog of War layer and display fog', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Fog of War');
const hasFog = await page.evaluate(() => {
const fogCanvas = document.getElementById('fog');
return fogCanvas && fogCanvas instanceof HTMLCanvasElement;
});
expect(hasFog).toBe(true);
});
test('should enable Areas layer and display areas', async ({ page }) => {
await waitForMap(page);
const hasAreasLayer = await page.evaluate(() => {
const mapElement = document.querySelector('#map');
const app = window.Stimulus;
const controller = app?.getControllerForElementAndIdentifier(mapElement, 'maps');
return controller?.areasLayer !== null && controller?.areasLayer !== undefined;
});
expect(hasAreasLayer).toBe(true);
});
test('should enable Suggested Visits layer', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Suggested Visits');
const hasSuggestedVisits = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.visitCircles !== null &&
controller?.visitsManager?.visitCircles !== undefined;
});
expect(hasSuggestedVisits).toBe(true);
});
test('should enable Confirmed Visits layer', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Confirmed Visits');
const hasConfirmedVisits = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.confirmedVisitCircles !== null &&
controller?.visitsManager?.confirmedVisitCircles !== undefined;
});
expect(hasConfirmedVisits).toBe(true);
});
test('should enable Scratch Map layer and display visited countries', async ({ page }) => {
await waitForMap(page);
await enableLayer(page, 'Scratch Map');
// Wait a bit for the layer to load country borders
await page.waitForTimeout(2000);
// Verify scratch layer exists and has been initialized
const hasScratchLayer = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
// Check if scratchLayerManager exists
if (!controller?.scratchLayerManager) return false;
// Check if scratch layer was created
const scratchLayer = controller.scratchLayerManager.getLayer();
return scratchLayer !== null && scratchLayer !== undefined;
});
expect(hasScratchLayer).toBe(true);
});
test('should remember enabled layers across page reloads', async ({ page }) => {
await waitForMap(page);
// Enable multiple layers
await enableLayer(page, 'Points');
await enableLayer(page, 'Routes');
await enableLayer(page, 'Heatmap');
await page.waitForTimeout(500);
// Get current layer states
const getLayerStates = () => page.evaluate(() => {
const layers = {};
document.querySelectorAll('.leaflet-control-layers-overlays input[type="checkbox"]').forEach(checkbox => {
const label = checkbox.parentElement.textContent.trim();
layers[label] = checkbox.checked;
});
return layers;
});
const layersBeforeReload = await getLayerStates();
// Reload the page
await page.reload();
await closeOnboardingModal(page);
await waitForMap(page);
await page.waitForTimeout(1000); // Wait for layers to restore
// Get layer states after reload
const layersAfterReload = await getLayerStates();
// Verify Points, Routes, and Heatmap are still enabled
expect(layersAfterReload['Points']).toBe(true);
expect(layersAfterReload['Routes']).toBe(true);
expect(layersAfterReload['Heatmap']).toBe(true);
// Verify layer states match before and after
expect(layersAfterReload).toEqual(layersBeforeReload);
});
});

141
e2e/map/map-points.spec.js Normal file
View file

@ -0,0 +1,141 @@
import { test, expect } from '@playwright/test';
import { navigateToMap } from '../helpers/navigation.js';
import { waitForMap, enableLayer } from '../helpers/map.js';
test.describe('Point Interactions', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
await enableLayer(page, 'Points');
await page.waitForTimeout(1500);
// Pan map to ensure a marker is in viewport
await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.markers && controller.markers.length > 0) {
const firstMarker = controller.markers[0];
controller.map.setView([firstMarker[0], firstMarker[1]], 14);
}
});
await page.waitForTimeout(1000);
});
test('should have draggable markers on the map', async ({ page }) => {
// Verify markers have draggable class
const marker = page.locator('.leaflet-marker-icon').first();
await expect(marker).toBeVisible();
// Check if marker has draggable class
const isDraggable = await marker.evaluate((el) => {
return el.classList.contains('leaflet-marker-draggable');
});
expect(isDraggable).toBe(true);
// Verify marker position can be retrieved (required for drag operations)
const box = await marker.boundingBox();
expect(box).not.toBeNull();
expect(box.x).toBeGreaterThan(0);
expect(box.y).toBeGreaterThan(0);
});
test('should open popup when clicking a point', async ({ page }) => {
// Click on a marker with force to ensure interaction
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible();
});
test('should display correct popup content with point data', async ({ page }) => {
// Click on a marker
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Get popup content
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent).toBeVisible();
const content = await popupContent.textContent();
// Verify all required fields are present
expect(content).toContain('Timestamp:');
expect(content).toContain('Latitude:');
expect(content).toContain('Longitude:');
expect(content).toContain('Altitude:');
expect(content).toContain('Speed:');
expect(content).toContain('Battery:');
expect(content).toContain('Id:');
});
test('should delete a point and redraw route', async ({ page }) => {
// Enable Routes layer to verify route redraw
await enableLayer(page, 'Routes');
await page.waitForTimeout(1000);
// Count initial markers and get point ID
const initialData = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
const markerCount = controller?.markersLayer ? Object.keys(controller.markersLayer._layers).length : 0;
const polylineCount = controller?.polylinesLayer ? Object.keys(controller.polylinesLayer._layers).length : 0;
return { markerCount, polylineCount };
});
// Click on a marker to open popup
const marker = page.locator('.leaflet-marker-icon').first();
await marker.click({ force: true });
await page.waitForTimeout(500);
// Verify popup opened
await expect(page.locator('.leaflet-popup')).toBeVisible();
// Get the point ID from popup before deleting
const pointId = await page.locator('.leaflet-popup-content').evaluate((content) => {
const match = content.textContent.match(/Id:\s*(\d+)/);
return match ? match[1] : null;
});
expect(pointId).not.toBeNull();
// Find delete button (might be a link or button with "Delete" text)
const deleteButton = page.locator('.leaflet-popup-content a:has-text("Delete"), .leaflet-popup-content button:has-text("Delete")').first();
const hasDeleteButton = await deleteButton.count() > 0;
if (hasDeleteButton) {
// Handle confirmation dialog
page.once('dialog', dialog => {
expect(dialog.message()).toContain('delete');
dialog.accept();
});
await deleteButton.click();
await page.waitForTimeout(2000); // Wait for deletion to complete
// Verify marker count decreased
const finalData = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
const markerCount = controller?.markersLayer ? Object.keys(controller.markersLayer._layers).length : 0;
const polylineCount = controller?.polylinesLayer ? Object.keys(controller.polylinesLayer._layers).length : 0;
return { markerCount, polylineCount };
});
// Verify at least one marker was removed
expect(finalData.markerCount).toBeLessThan(initialData.markerCount);
// Verify routes still exist (they should be redrawn)
expect(finalData.polylineCount).toBeGreaterThanOrEqual(0);
// Verify success flash message appears
const flashMessage = page.locator('#flash-messages [role="alert"]').filter({ hasText: /deleted successfully/i });
await expect(flashMessage).toBeVisible({ timeout: 5000 });
} else {
// If no delete button, just verify the test setup worked
console.log('No delete button found in popup - this might be expected based on permissions');
}
});
});

View file

@ -0,0 +1,166 @@
import { test, expect } from '@playwright/test';
import { navigateToMap } from '../helpers/navigation.js';
import { waitForMap } from '../helpers/map.js';
test.describe('Selection Tool', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
});
test('should enable selection mode when clicked', async ({ page }) => {
// Click selection tool button
const selectionButton = page.locator('#selection-tool-button');
await expect(selectionButton).toBeVisible();
await selectionButton.click();
await page.waitForTimeout(500);
// Verify selection mode is enabled (flash message appears)
const flashMessage = page.locator('#flash-messages [role="alert"]:has-text("Selection mode enabled")');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// Verify selection mode is active in controller
const isSelectionActive = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.isSelectionActive === true;
});
expect(isSelectionActive).toBe(true);
// Verify button has active class
const hasActiveClass = await selectionButton.evaluate((el) => {
return el.classList.contains('active');
});
expect(hasActiveClass).toBe(true);
// Verify map dragging is disabled (required for selection to work)
const isDraggingDisabled = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return !controller?.map?.dragging?.enabled();
});
expect(isDraggingDisabled).toBe(true);
});
test('should disable selection mode when clicked second time', async ({ page }) => {
const selectionButton = page.locator('#selection-tool-button');
// First click - enable selection mode
await selectionButton.click();
await page.waitForTimeout(500);
// Verify selection mode is enabled
const isEnabledAfterFirstClick = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.isSelectionActive === true;
});
expect(isEnabledAfterFirstClick).toBe(true);
// Second click - disable selection mode
await selectionButton.click();
await page.waitForTimeout(500);
// Verify selection mode is disabled
const isDisabledAfterSecondClick = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.isSelectionActive === false;
});
expect(isDisabledAfterSecondClick).toBe(true);
// Verify no selection rectangle exists
const hasSelectionRect = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.visitsManager?.selectionRect !== null;
});
expect(hasSelectionRect).toBe(false);
// Verify button no longer has active class
const hasActiveClass = await selectionButton.evaluate((el) => {
return el.classList.contains('active');
});
expect(hasActiveClass).toBe(false);
// Verify map dragging is re-enabled
const isDraggingEnabled = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.map?.dragging?.enabled();
});
expect(isDraggingEnabled).toBe(true);
});
test('should show info message about dragging to select area', async ({ page }) => {
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
// Verify informational flash message about dragging
const flashMessage = page.locator('#flash-messages [role="alert"]');
const messageText = await flashMessage.textContent();
expect(messageText).toContain('Click and drag');
});
test('should open side panel when selection is complete', async ({ page }) => {
// Navigate to a date with known data (October 13, 2024 - same as bulk delete tests)
const startInput = page.locator('input[type="datetime-local"][name="start_at"]');
await startInput.clear();
await startInput.fill('2024-10-13T00:00');
const endInput = page.locator('input[type="datetime-local"][name="end_at"]');
await endInput.clear();
await endInput.fill('2024-10-13T23:59');
await page.click('input[type="submit"][value="Search"]');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify drawer is initially closed
const drawerInitiallyClosed = await page.evaluate(() => {
const drawer = document.getElementById('visits-drawer');
return !drawer?.classList.contains('open');
});
expect(drawerInitiallyClosed).toBe(true);
// Enable selection mode
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
// Draw a selection rectangle on the map
const mapContainer = page.locator('#map [data-maps-target="container"]');
const bbox = await mapContainer.boundingBox();
// Draw rectangle covering most of the map to ensure we select points
const startX = bbox.x + bbox.width * 0.2;
const startY = bbox.y + bbox.height * 0.2;
const endX = bbox.x + bbox.width * 0.8;
const endY = bbox.y + bbox.height * 0.8;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY, { steps: 10 });
await page.mouse.up();
// Wait for drawer to open
await page.waitForTimeout(2000);
// Verify drawer is now open
const drawerOpen = await page.evaluate(() => {
const drawer = document.getElementById('visits-drawer');
return drawer?.classList.contains('open');
});
expect(drawerOpen).toBe(true);
// Verify drawer shows either selection data or cancel button (indicates selection is active)
const hasCancelButton = await page.locator('#cancel-selection-button').isVisible();
expect(hasCancelButton).toBe(true);
});
});

View file

@ -0,0 +1,538 @@
import { test, expect } from '@playwright/test';
import { closeOnboardingModal, navigateToDate } from '../helpers/navigation.js';
import { drawSelectionRectangle } from '../helpers/selection.js';
/**
* Side Panel (Visits Drawer) Tests
*
* Tests for the side panel that displays visits when selection tool is used.
* The panel can be toggled via the drawer button and shows suggested/confirmed visits
* with options to confirm, decline, or merge them.
*/
test.describe('Side Panel', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/map');
await closeOnboardingModal(page);
// Wait for map to be fully loaded
await page.waitForSelector('.leaflet-container', { state: 'visible', timeout: 10000 });
await page.waitForTimeout(2000);
// Navigate to October 2024 (has demo data)
await navigateToDate(page, '2024-10-01T00:00', '2024-10-31T23:59');
await page.waitForTimeout(2000);
});
/**
* Helper function to click the drawer button
*/
async function clickDrawerButton(page) {
const drawerButton = page.locator('.drawer-button');
await expect(drawerButton).toBeVisible({ timeout: 5000 });
await drawerButton.click();
await page.waitForTimeout(500); // Wait for drawer animation
}
/**
* Helper function to check if drawer is open
*/
async function isDrawerOpen(page) {
const drawer = page.locator('#visits-drawer');
const exists = await drawer.count() > 0;
if (!exists) return false;
const hasOpenClass = await drawer.evaluate(el => el.classList.contains('open'));
return hasOpenClass;
}
/**
* Helper function to perform selection and wait for visits to load
* This is a simplified version that doesn't use the shared helper
* because we need custom waiting logic for the drawer
*/
async function selectAreaWithVisits(page) {
// First, enable Suggested Visits layer to ensure visits are loaded
const layersButton = page.locator('.leaflet-control-layers-toggle');
await layersButton.click();
await page.waitForTimeout(500);
// Enable "Suggested Visits" layer
const suggestedVisitsCheckbox = page.locator('input[type="checkbox"]').filter({
has: page.locator(':scope ~ span', { hasText: 'Suggested Visits' })
});
const isChecked = await suggestedVisitsCheckbox.isChecked();
if (!isChecked) {
await suggestedVisitsCheckbox.check();
await page.waitForTimeout(1000);
}
// Close layers control
await layersButton.click();
await page.waitForTimeout(500);
// Enable selection mode
const selectionButton = page.locator('#selection-tool-button');
await selectionButton.click();
await page.waitForTimeout(500);
// Get map bounds for drawing selection
const map = page.locator('.leaflet-container');
const mapBox = await map.boundingBox();
// Calculate coordinates for drawing a large selection area
// Make it much wider to catch visits - use most of the map area
const startX = mapBox.x + 100;
const startY = mapBox.y + 100;
const endX = mapBox.x + mapBox.width - 400; // Leave room for drawer on right
const endY = mapBox.y + mapBox.height - 100;
// Draw selection rectangle
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(endX, endY, { steps: 10 });
await page.mouse.up();
// Wait for drawer to be created and opened
await page.waitForSelector('#visits-drawer.open', { timeout: 10000 });
await page.waitForTimeout(3000); // Wait longer for visits API response
}
test('should open and close drawer panel via button click', async ({ page }) => {
// Verify drawer is initially closed
const initiallyOpen = await isDrawerOpen(page);
expect(initiallyOpen).toBe(false);
// Click to open
await clickDrawerButton(page);
// Verify drawer is now open
let drawerOpen = await isDrawerOpen(page);
expect(drawerOpen).toBe(true);
// Verify drawer content is visible
const drawerContent = page.locator('#visits-drawer .drawer');
await expect(drawerContent).toBeVisible();
// Click to close
await clickDrawerButton(page);
// Verify drawer is now closed
drawerOpen = await isDrawerOpen(page);
expect(drawerOpen).toBe(false);
});
test('should show visits in panel after selection', async ({ page }) => {
await selectAreaWithVisits(page);
// Verify drawer is open
const drawerOpen = await isDrawerOpen(page);
expect(drawerOpen).toBe(true);
// Verify visits list container exists
const visitsList = page.locator('#visits-list');
await expect(visitsList).toBeVisible();
// Verify at least one visit is displayed
const visitItems = page.locator('.visit-item');
const visitCount = await visitItems.count();
expect(visitCount).toBeGreaterThan(0);
// Verify drawer title shows count
const drawerTitle = page.locator('#visits-drawer .drawer h2');
const titleText = await drawerTitle.textContent();
expect(titleText).toMatch(/\d+ visits? found/);
});
test('should display visit details in panel', async ({ page }) => {
await selectAreaWithVisits(page);
// Get first visit item
const firstVisit = page.locator('.visit-item').first();
await expect(firstVisit).toBeVisible();
// Verify visit has required information
const visitName = firstVisit.locator('.font-semibold');
await expect(visitName).toBeVisible();
const nameText = await visitName.textContent();
expect(nameText.length).toBeGreaterThan(0);
// Verify time information is present
const timeInfo = firstVisit.locator('.text-sm.text-gray-600');
await expect(timeInfo).toBeVisible();
// Check if this is a suggested visit (has confirm/decline buttons)
const hasSuggestedButtons = await firstVisit.locator('.confirm-visit').count() > 0;
if (hasSuggestedButtons) {
// For suggested visits, verify action buttons are present
const confirmButton = firstVisit.locator('.confirm-visit');
const declineButton = firstVisit.locator('.decline-visit');
await expect(confirmButton).toBeVisible();
await expect(declineButton).toBeVisible();
expect(await confirmButton.textContent()).toBe('Confirm');
expect(await declineButton.textContent()).toBe('Decline');
}
});
test('should confirm individual suggested visit from panel', async ({ page }) => {
await selectAreaWithVisits(page);
// Find a suggested visit (one with confirm/decline buttons)
const suggestedVisit = page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') }).first();
// Check if any suggested visits exist
const suggestedCount = await page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') }).count();
if (suggestedCount === 0) {
console.log('Test skipped: No suggested visits available');
test.skip();
return;
}
await expect(suggestedVisit).toBeVisible();
// Verify it has the suggested visit styling (dashed border)
const hasDashedBorder = await suggestedVisit.evaluate(el =>
el.classList.contains('border-dashed')
);
expect(hasDashedBorder).toBe(true);
// Get initial count of visits
const initialVisitCount = await page.locator('.visit-item').count();
// Click confirm button
const confirmButton = suggestedVisit.locator('.confirm-visit');
await confirmButton.click();
// Wait for API call and UI update
await page.waitForTimeout(2000);
// Verify flash message appears
const flashMessage = page.locator('.flash-message');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// The visit should still be in the list but without confirm/decline buttons
// Or the count might decrease if it was removed from suggested visits
const finalVisitCount = await page.locator('.visit-item').count();
expect(finalVisitCount).toBeLessThanOrEqual(initialVisitCount);
});
test('should decline individual suggested visit from panel', async ({ page }) => {
await selectAreaWithVisits(page);
// Find a suggested visit
const suggestedVisit = page.locator('.visit-item').filter({ has: page.locator('.decline-visit') }).first();
const suggestedCount = await page.locator('.visit-item').filter({ has: page.locator('.decline-visit') }).count();
if (suggestedCount === 0) {
console.log('Test skipped: No suggested visits available');
test.skip();
return;
}
await expect(suggestedVisit).toBeVisible();
// Get initial count
const initialVisitCount = await page.locator('.visit-item').count();
// Click decline button
const declineButton = suggestedVisit.locator('.decline-visit');
await declineButton.click();
// Wait for API call and UI update
await page.waitForTimeout(2000);
// Verify flash message
const flashMessage = page.locator('.flash-message');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// Visit should be removed from the list
const finalVisitCount = await page.locator('.visit-item').count();
expect(finalVisitCount).toBeLessThan(initialVisitCount);
});
test('should show checkboxes on hover for mass selection', async ({ page }) => {
await selectAreaWithVisits(page);
const firstVisit = page.locator('.visit-item').first();
await expect(firstVisit).toBeVisible();
// Initially, checkbox should be hidden
const checkboxContainer = firstVisit.locator('.visit-checkbox-container');
let opacity = await checkboxContainer.evaluate(el => el.style.opacity);
expect(opacity === '0' || opacity === '').toBe(true);
// Hover over the visit item
await firstVisit.hover();
await page.waitForTimeout(300);
// Checkbox should now be visible
opacity = await checkboxContainer.evaluate(el => el.style.opacity);
expect(opacity).toBe('1');
// Checkbox should be clickable
const pointerEvents = await checkboxContainer.evaluate(el => el.style.pointerEvents);
expect(pointerEvents).toBe('auto');
});
test('should select multiple visits and show bulk action buttons', async ({ page }) => {
await selectAreaWithVisits(page);
// Verify we have at least 2 visits
const visitCount = await page.locator('.visit-item').count();
if (visitCount < 2) {
console.log('Test skipped: Need at least 2 visits');
test.skip();
return;
}
// Select first visit by hovering and clicking checkbox
const firstVisit = page.locator('.visit-item').first();
await firstVisit.hover();
await page.waitForTimeout(300);
const firstCheckbox = firstVisit.locator('.visit-checkbox');
await firstCheckbox.click();
await page.waitForTimeout(500);
// Select second visit
const secondVisit = page.locator('.visit-item').nth(1);
await secondVisit.hover();
await page.waitForTimeout(300);
const secondCheckbox = secondVisit.locator('.visit-checkbox');
await secondCheckbox.click();
await page.waitForTimeout(500);
// Verify bulk action buttons appear
const bulkActionsContainer = page.locator('.visit-bulk-actions');
await expect(bulkActionsContainer).toBeVisible();
// Verify all three action buttons are present
const mergeButton = bulkActionsContainer.locator('button').filter({ hasText: 'Merge' });
const confirmButton = bulkActionsContainer.locator('button').filter({ hasText: 'Confirm' });
const declineButton = bulkActionsContainer.locator('button').filter({ hasText: 'Decline' });
await expect(mergeButton).toBeVisible();
await expect(confirmButton).toBeVisible();
await expect(declineButton).toBeVisible();
// Verify selection count text
const selectionText = bulkActionsContainer.locator('.text-sm.text-center');
const selectionTextContent = await selectionText.textContent();
expect(selectionTextContent).toContain('2 visits selected');
// Verify cancel button exists
const cancelButton = bulkActionsContainer.locator('button').filter({ hasText: 'Cancel Selection' });
await expect(cancelButton).toBeVisible();
});
test('should cancel mass selection', async ({ page }) => {
await selectAreaWithVisits(page);
const visitCount = await page.locator('.visit-item').count();
if (visitCount < 2) {
console.log('Test skipped: Need at least 2 visits');
test.skip();
return;
}
// Select two visits
const firstVisit = page.locator('.visit-item').first();
await firstVisit.hover();
await page.waitForTimeout(300);
await firstVisit.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
const secondVisit = page.locator('.visit-item').nth(1);
await secondVisit.hover();
await page.waitForTimeout(300);
await secondVisit.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
// Verify bulk actions are visible
const bulkActions = page.locator('.visit-bulk-actions');
await expect(bulkActions).toBeVisible();
// Click cancel button
const cancelButton = bulkActions.locator('button').filter({ hasText: 'Cancel Selection' });
await cancelButton.click();
await page.waitForTimeout(500);
// Verify bulk actions are removed
await expect(bulkActions).not.toBeVisible();
// Verify checkboxes are unchecked
const checkedCheckboxes = await page.locator('.visit-checkbox:checked').count();
expect(checkedCheckboxes).toBe(0);
});
test('should mass confirm multiple visits', async ({ page }) => {
await selectAreaWithVisits(page);
// Find suggested visits (those with confirm buttons)
const suggestedVisits = page.locator('.visit-item').filter({ has: page.locator('.confirm-visit') });
const suggestedCount = await suggestedVisits.count();
if (suggestedCount < 2) {
console.log('Test skipped: Need at least 2 suggested visits');
test.skip();
return;
}
// Get initial count
const initialVisitCount = await page.locator('.visit-item').count();
// Select first two suggested visits
const firstSuggested = suggestedVisits.first();
await firstSuggested.hover();
await page.waitForTimeout(300);
await firstSuggested.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
const secondSuggested = suggestedVisits.nth(1);
await secondSuggested.hover();
await page.waitForTimeout(300);
await secondSuggested.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
// Click mass confirm button
const bulkActions = page.locator('.visit-bulk-actions');
const confirmButton = bulkActions.locator('button').filter({ hasText: 'Confirm' });
await confirmButton.click();
// Wait for API call
await page.waitForTimeout(2000);
// Verify flash message
const flashMessage = page.locator('.flash-message');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// The visits might be removed or updated in the list
// At minimum, bulk actions should be removed
const bulkActionsVisible = await bulkActions.isVisible().catch(() => false);
expect(bulkActionsVisible).toBe(false);
});
test('should mass decline multiple visits', async ({ page }) => {
await selectAreaWithVisits(page);
const suggestedVisits = page.locator('.visit-item').filter({ has: page.locator('.decline-visit') });
const suggestedCount = await suggestedVisits.count();
if (suggestedCount < 2) {
console.log('Test skipped: Need at least 2 suggested visits');
test.skip();
return;
}
// Get initial count
const initialVisitCount = await page.locator('.visit-item').count();
// Select two visits
const firstSuggested = suggestedVisits.first();
await firstSuggested.hover();
await page.waitForTimeout(300);
await firstSuggested.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
const secondSuggested = suggestedVisits.nth(1);
await secondSuggested.hover();
await page.waitForTimeout(300);
await secondSuggested.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
// Click mass decline button
const bulkActions = page.locator('.visit-bulk-actions');
const declineButton = bulkActions.locator('button').filter({ hasText: 'Decline' });
await declineButton.click();
// Wait for API call
await page.waitForTimeout(2000);
// Verify flash message
const flashMessage = page.locator('.flash-message');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// Visits should be removed from the list
const finalVisitCount = await page.locator('.visit-item').count();
expect(finalVisitCount).toBeLessThan(initialVisitCount);
});
test('should mass merge multiple visits', async ({ page }) => {
await selectAreaWithVisits(page);
const visitCount = await page.locator('.visit-item').count();
if (visitCount < 2) {
console.log('Test skipped: Need at least 2 visits');
test.skip();
return;
}
// Select two visits
const firstVisit = page.locator('.visit-item').first();
await firstVisit.hover();
await page.waitForTimeout(300);
await firstVisit.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
const secondVisit = page.locator('.visit-item').nth(1);
await secondVisit.hover();
await page.waitForTimeout(300);
await secondVisit.locator('.visit-checkbox').click();
await page.waitForTimeout(500);
// Click merge button
const bulkActions = page.locator('.visit-bulk-actions');
const mergeButton = bulkActions.locator('button').filter({ hasText: 'Merge' });
await mergeButton.click();
// Wait for API call
await page.waitForTimeout(2000);
// Verify flash message appears
const flashMessage = page.locator('.flash-message');
await expect(flashMessage).toBeVisible({ timeout: 5000 });
// After merge, the visits should be combined into one
// So final count should be less than initial
const finalVisitCount = await page.locator('.visit-item').count();
expect(finalVisitCount).toBeLessThan(visitCount);
});
test('should shift controls when panel opens and shift back when closed', async ({ page }) => {
// Get initial position of a control element (layer control)
const layerControl = page.locator('.leaflet-control-layers');
await expect(layerControl).toBeVisible();
// Check if controls have the shifted class initially (should not)
const initiallyShifted = await layerControl.evaluate(el =>
el.classList.contains('controls-shifted')
);
expect(initiallyShifted).toBe(false);
// Open the drawer
await clickDrawerButton(page);
await page.waitForTimeout(500);
// Verify controls now have the shifted class
const shiftedAfterOpen = await layerControl.evaluate(el =>
el.classList.contains('controls-shifted')
);
expect(shiftedAfterOpen).toBe(true);
// Close the drawer
await clickDrawerButton(page);
await page.waitForTimeout(500);
// Verify controls no longer have the shifted class
const shiftedAfterClose = await layerControl.evaluate(el =>
el.classList.contains('controls-shifted')
);
expect(shiftedAfterClose).toBe(false);
});
});

View file

@ -0,0 +1,296 @@
import { test, expect } from '@playwright/test';
import { navigateToMap, closeOnboardingModal } from '../helpers/navigation.js';
import { waitForMap, enableLayer, clickSuggestedVisit } from '../helpers/map.js';
test.describe('Suggested Visit Interactions', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
// Navigate to a date range that includes visits (last month to now)
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
// Set date range to last month
await page.click('a:has-text("Last month")');
await page.waitForTimeout(2000);
await closeOnboardingModal(page);
await waitForMap(page);
await enableLayer(page, 'Suggested Visits');
await page.waitForTimeout(2000);
// Pan map to ensure a visit marker is in viewport
await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles) {
const layers = controller.visitsManager.suggestedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit && firstVisit._latlng) {
controller.map.setView(firstVisit._latlng, 14);
}
}
});
await page.waitForTimeout(1000);
});
test('should click on a suggested visit and open popup', async ({ page }) => {
// Debug: Check what visit circles exist
const allCircles = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
const layers = controller.visitsManager.suggestedVisitCircles._layers;
return {
count: Object.keys(layers).length,
hasLayers: Object.keys(layers).length > 0
};
}
return { count: 0, hasLayers: false };
});
// If we have visits in the layer but can't find DOM elements, use coordinates
if (!allCircles.hasLayers) {
console.log('No suggested visits found - skipping test');
return;
}
// Click on the visit using map coordinates
const visitClicked = await clickSuggestedVisit(page);
if (!visitClicked) {
console.log('Could not click suggested visit - skipping test');
return;
}
await page.waitForTimeout(500);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible();
});
test('should display correct content in suggested visit popup', async ({ page }) => {
// Click visit programmatically
const visitClicked = await clickSuggestedVisit(page);
if (!visitClicked) {
console.log('No suggested visits found - skipping test');
return;
}
await page.waitForTimeout(500);
// Get popup content
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent).toBeVisible();
const content = await popupContent.textContent();
// Verify visit information is present
expect(content).toMatch(/Visit|Place|Duration|Started|Ended|Suggested/i);
});
test('should confirm suggested visit', async ({ page }) => {
// Click visit programmatically
const visitClicked = await clickSuggestedVisit(page);
if (!visitClicked) {
console.log('No suggested visits found - skipping test');
return;
}
await page.waitForTimeout(500);
// Look for confirm button in popup
const confirmButton = page.locator('.leaflet-popup-content button:has-text("Confirm")').first();
const hasConfirmButton = await confirmButton.count() > 0;
if (!hasConfirmButton) {
console.log('No confirm button found - skipping test');
return;
}
// Get initial counts for both suggested and confirmed visits
const initialCounts = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return {
suggested: controller?.visitsManager?.suggestedVisitCircles?._layers
? Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length
: 0,
confirmed: controller?.visitsManager?.confirmedVisitCircles?._layers
? Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length
: 0
};
});
// Click confirm button
await confirmButton.click();
await page.waitForTimeout(1500);
// Verify the marker changed from yellow to green (suggested to confirmed)
const finalCounts = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return {
suggested: controller?.visitsManager?.suggestedVisitCircles?._layers
? Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length
: 0,
confirmed: controller?.visitsManager?.confirmedVisitCircles?._layers
? Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length
: 0
};
});
// Verify suggested visit count decreased
expect(finalCounts.suggested).toBeLessThan(initialCounts.suggested);
// Verify confirmed visit count increased (marker changed from yellow to green)
expect(finalCounts.confirmed).toBeGreaterThan(initialCounts.confirmed);
// Verify popup is closed after confirmation
const popupVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
expect(popupVisible).toBe(false);
});
test('should decline suggested visit', async ({ page }) => {
// Click visit programmatically
const visitClicked = await clickSuggestedVisit(page);
if (!visitClicked) {
console.log('No suggested visits found - skipping test');
return;
}
await page.waitForTimeout(500);
// Look for decline button in popup
const declineButton = page.locator('.leaflet-popup-content button:has-text("Decline")').first();
const hasDeclineButton = await declineButton.count() > 0;
if (!hasDeclineButton) {
console.log('No decline button found - skipping test');
return;
}
// Get initial suggested visit count
const initialCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length;
}
return 0;
});
// Verify popup is visible before decline
await expect(page.locator('.leaflet-popup')).toBeVisible();
// Click decline button
await declineButton.click();
await page.waitForTimeout(1500);
// Verify popup is removed from map
const popupVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
expect(popupVisible).toBe(false);
// Verify marker is removed from map (suggested visit count decreased)
const finalCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length;
}
return 0;
});
expect(finalCount).toBeLessThan(initialCount);
// Verify the yellow marker is no longer visible on the map
const yellowMarkerCount = await page.locator('.leaflet-interactive[stroke="#f59e0b"]').count();
expect(yellowMarkerCount).toBeLessThan(initialCount);
});
test('should change place in dropdown for suggested visit', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#f59e0b"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No suggested visits found - skipping test');
return;
}
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Look for place dropdown/select in popup
const placeSelect = page.locator('.leaflet-popup-content select, .leaflet-popup-content [role="combobox"]').first();
const hasPlaceDropdown = await placeSelect.count() > 0;
if (!hasPlaceDropdown) {
console.log('No place dropdown found - skipping test');
return;
}
// Select a different option
await placeSelect.selectOption({ index: 1 });
await page.waitForTimeout(300);
// Verify the selection changed
const newValue = await placeSelect.inputValue();
expect(newValue).toBeTruthy();
});
test('should delete suggested visit from map', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#f59e0b"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No suggested visits found - skipping test');
return;
}
// Count initial visits
const initialVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length;
}
return 0;
});
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Find delete button
const deleteButton = page.locator('.leaflet-popup-content button:has-text("Delete"), .leaflet-popup-content a:has-text("Delete")').first();
const hasDeleteButton = await deleteButton.count() > 0;
if (!hasDeleteButton) {
console.log('No delete button found - skipping test');
return;
}
// Handle confirmation dialog
page.once('dialog', dialog => {
expect(dialog.message()).toMatch(/delete|remove/i);
dialog.accept();
});
await deleteButton.click();
await page.waitForTimeout(2000);
// Verify visit count decreased
const finalVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.suggestedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.suggestedVisitCircles._layers).length;
}
return 0;
});
expect(finalVisitCount).toBeLessThan(initialVisitCount);
});
});

232
e2e/map/map-visits.spec.js Normal file
View file

@ -0,0 +1,232 @@
import { test, expect } from '@playwright/test';
import { navigateToMap, closeOnboardingModal } from '../helpers/navigation.js';
import { waitForMap, enableLayer, clickConfirmedVisit } from '../helpers/map.js';
test.describe('Visit Interactions', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
// Navigate to a date range that includes visits (last month to now)
const toggleButton = page.locator('button[data-action*="map-controls#toggle"]');
const isPanelVisible = await page.locator('[data-map-controls-target="panel"]').isVisible();
if (!isPanelVisible) {
await toggleButton.click();
await page.waitForTimeout(300);
}
// Set date range to last month
await page.click('a:has-text("Last month")');
await page.waitForTimeout(2000);
await closeOnboardingModal(page);
await waitForMap(page);
await enableLayer(page, 'Confirmed Visits');
await page.waitForTimeout(2000);
// Pan map to ensure a visit marker is in viewport
await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
const firstVisit = Object.values(layers)[0];
if (firstVisit && firstVisit._latlng) {
controller.map.setView(firstVisit._latlng, 14);
}
}
});
await page.waitForTimeout(1000);
});
test('should click on a confirmed visit and open popup', async ({ page }) => {
// Debug: Check what visit circles exist
const allCircles = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
const layers = controller.visitsManager.confirmedVisitCircles._layers;
return {
count: Object.keys(layers).length,
hasLayers: Object.keys(layers).length > 0
};
}
return { count: 0, hasLayers: false };
});
// If we have visits in the layer but can't find DOM elements, use coordinates
if (!allCircles.hasLayers) {
console.log('No confirmed visits found - skipping test');
return;
}
// Click on the visit using map coordinates
const visitClicked = await clickConfirmedVisit(page);
if (!visitClicked) {
console.log('Could not click visit - skipping test');
return;
}
await page.waitForTimeout(500);
// Verify popup is visible
const popup = page.locator('.leaflet-popup');
await expect(popup).toBeVisible();
});
test('should display correct content in confirmed visit popup', async ({ page }) => {
// Click visit programmatically
const visitClicked = await clickConfirmedVisit(page);
if (!visitClicked) {
console.log('No confirmed visits found - skipping test');
return;
}
await page.waitForTimeout(500);
// Get popup content
const popupContent = page.locator('.leaflet-popup-content');
await expect(popupContent).toBeVisible();
const content = await popupContent.textContent();
// Verify visit information is present
expect(content).toMatch(/Visit|Place|Duration|Started|Ended/i);
});
test('should change place in dropdown and save', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Look for place dropdown/select in popup
const placeSelect = page.locator('.leaflet-popup-content select, .leaflet-popup-content [role="combobox"]').first();
const hasPlaceDropdown = await placeSelect.count() > 0;
if (!hasPlaceDropdown) {
console.log('No place dropdown found - skipping test');
return;
}
// Get current value
const initialValue = await placeSelect.inputValue().catch(() => null);
// Select a different option
await placeSelect.selectOption({ index: 1 });
await page.waitForTimeout(300);
// Find and click save button
const saveButton = page.locator('.leaflet-popup-content button:has-text("Save"), .leaflet-popup-content input[type="submit"]').first();
const hasSaveButton = await saveButton.count() > 0;
if (hasSaveButton) {
await saveButton.click();
await page.waitForTimeout(1000);
// Verify success message or popup closes
const popupStillVisible = await page.locator('.leaflet-popup').isVisible().catch(() => false);
// Either popup closes or stays open with updated content
expect(popupStillVisible === false || popupStillVisible === true).toBe(true);
}
});
test('should change visit name and save', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Look for name input field
const nameInput = page.locator('.leaflet-popup-content input[type="text"]').first();
const hasNameInput = await nameInput.count() > 0;
if (!hasNameInput) {
console.log('No name input found - skipping test');
return;
}
// Change the name
const newName = `Test Visit ${Date.now()}`;
await nameInput.fill(newName);
await page.waitForTimeout(300);
// Find and click save button
const saveButton = page.locator('.leaflet-popup-content button:has-text("Save"), .leaflet-popup-content input[type="submit"]').first();
const hasSaveButton = await saveButton.count() > 0;
if (hasSaveButton) {
await saveButton.click();
await page.waitForTimeout(1000);
// Verify flash message or popup closes
const flashOrClose = await page.locator('#flash-messages [role="alert"]').isVisible({ timeout: 2000 }).catch(() => false);
expect(flashOrClose === true || flashOrClose === false).toBe(true);
}
});
test('should delete confirmed visit from map', async ({ page }) => {
const visitCircle = page.locator('.leaflet-interactive[stroke="#10b981"]').first();
const hasVisits = await visitCircle.count() > 0;
if (!hasVisits) {
console.log('No confirmed visits found - skipping test');
return;
}
// Count initial visits
const initialVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
await visitCircle.click({ force: true });
await page.waitForTimeout(500);
// Find delete button
const deleteButton = page.locator('.leaflet-popup-content button:has-text("Delete"), .leaflet-popup-content a:has-text("Delete")').first();
const hasDeleteButton = await deleteButton.count() > 0;
if (!hasDeleteButton) {
console.log('No delete button found - skipping test');
return;
}
// Handle confirmation dialog
page.once('dialog', dialog => {
expect(dialog.message()).toMatch(/delete|remove/i);
dialog.accept();
});
await deleteButton.click();
await page.waitForTimeout(2000);
// Verify visit count decreased
const finalVisitCount = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller?.visitsManager?.confirmedVisitCircles?._layers) {
return Object.keys(controller.visitsManager.confirmedVisitCircles._layers).length;
}
return 0;
});
expect(finalVisitCount).toBeLessThan(initialVisitCount);
});
});

View file

@ -2,7 +2,7 @@
"cookies": [
{
"name": "_dawarich_session",
"value": "uSq%2BCBWS9YujuNhicVcBHgR62tcYRgXVobd6flpFfMCPEGldb7vMuvaYDitf%2Fr%2FRMXC2Vb4VKE0dHz00pXp1S%2F6gBJuXHZ05is2zoZPGQ5gRaGVRbEG%2F%2FfKVSKgOb3B87WmmrB4I%2B1jq10hT0MI%2FKzeAhIR%2BI1hVmGYeozx%2BNttmWjIgtYk%2FN9JnsN7jzmYvON65mrRgJQyPftaIEpOYpMCdbPl1uosRoQpO6WroGxQ4J89lFvoSbyTspqJje8A0i5JNJThSfRkyPcIPXNtIFGHUUBX376%2BOLZds7sLor6%2BMu%2FQs%2FnjQl2xWFxejo6lSp%2BZrplztjPwquQtjm1BmnWN1PPvFsrphEw2scIk%2BQUyk2F3ZQcVwBUExB0dm5qA9M3uSbvR%2BV8OU--8oAuBe0ygLxh0x4N--STVFL7XMmRGm17xka6AU3A%3D%3D",
"value": "nE8XtV%2FHfX8CsqZBe1QAQhkkPEZ7qf9kmdmWoGK54d5wo0Nddlnwr4FyCeNcZ2yTRCJl318iWzJgfPxDNVXvkMCRloueKh45K8jAmER08h6t6Z6zXPlIq93OomMeSxcpudmD7rVBVuXgPHYnBKr2LBS6624Iz20BtXQVnF63EabM6bpBufCoWzlD7uRjzy20bN%2BdYCofavYkgHaZQc9IX%2FmN%2BrJEH88%2BcSCukNRHl1b8VhSNIfYTYoqwaXV1DjPnNdfFhBXcytyUuH9BqkkEZQTYzBXWZijAnmtJpcBTzXTCl6q3wdBDBlQ9OKkzurG1ykIH9tCVwMTHv695GxGUbiDTI86AudDwU7HSQnQzJK5gDNbCK%2Fxrc8ngdgDrGJkL23AJK9Ue7uzG--FQMhYrNeTDFIPVKV--WFIyRKwPplKvoujIn1omBA%3D%3D",
"domain": "localhost",
"path": "/",
"expires": -1,

View file

@ -42,7 +42,7 @@ export default defineConfig({
// Setup project - runs authentication before all tests
{
name: 'setup',
testMatch: /auth\.setup\.js/
testMatch: /.*\/setup\/auth\.setup\.js/
},
{