mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Merge branch 'dev', remote-tracking branch 'origin' into fix/photos-layer
This commit is contained in:
commit
2f3ba0c8db
5 changed files with 934 additions and 22 deletions
|
|
@ -10,12 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
- Photos layer is now working again on the map page. #1563 #1421 #1071 #889
|
- Photos layer is now working again on the map page. #1563 #1421 #1071 #889
|
||||||
- Suggested and Confirmed visits layers are now working again on the map page. #1443
|
- Suggested and Confirmed visits layers are now working again on the map page. #1443
|
||||||
|
- Fog of war is now working correctly. #1583
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- Logging for Photos layer is now enabled.
|
- Logging for Photos layer is now enabled.
|
||||||
|
|
||||||
# [0.30.6] - 2025-07-27
|
|
||||||
|
# [0.30.6] - 2025-07-29
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export default class extends BaseController {
|
||||||
this.tracksLayer = L.layerGroup();
|
this.tracksLayer = L.layerGroup();
|
||||||
|
|
||||||
// Create a proper Leaflet layer for fog
|
// Create a proper Leaflet layer for fog
|
||||||
this.fogOverlay = createFogOverlay();
|
this.fogOverlay = new (createFogOverlay())();
|
||||||
|
|
||||||
// Create custom pane for areas
|
// Create custom pane for areas
|
||||||
this.map.createPane('areasPane');
|
this.map.createPane('areasPane');
|
||||||
|
|
@ -202,7 +202,7 @@ export default class extends BaseController {
|
||||||
Routes: this.polylinesLayer,
|
Routes: this.polylinesLayer,
|
||||||
Tracks: this.tracksLayer,
|
Tracks: this.tracksLayer,
|
||||||
Heatmap: this.heatmapLayer,
|
Heatmap: this.heatmapLayer,
|
||||||
"Fog of War": new this.fogOverlay(),
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer,
|
"Scratch map": this.scratchLayer,
|
||||||
Areas: this.areasLayer,
|
Areas: this.areasLayer,
|
||||||
Photos: this.photoMarkers,
|
Photos: this.photoMarkers,
|
||||||
|
|
@ -424,7 +424,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
async refreshScratchLayer() {
|
async refreshScratchLayer() {
|
||||||
console.log('Refreshing scratch layer with current data');
|
console.log('Refreshing scratch layer with current data');
|
||||||
|
|
||||||
if (!this.scratchLayer) {
|
if (!this.scratchLayer) {
|
||||||
console.log('Scratch layer not initialized, setting up');
|
console.log('Scratch layer not initialized, setting up');
|
||||||
await this.setupScratchLayer(this.countryCodesMap);
|
await this.setupScratchLayer(this.countryCodesMap);
|
||||||
|
|
@ -434,11 +434,11 @@ export default class extends BaseController {
|
||||||
try {
|
try {
|
||||||
// Clear existing data
|
// Clear existing data
|
||||||
this.scratchLayer.clearLayers();
|
this.scratchLayer.clearLayers();
|
||||||
|
|
||||||
// Get current visited countries based on current markers
|
// Get current visited countries based on current markers
|
||||||
const visitedCountries = this.getVisitedCountries(this.countryCodesMap);
|
const visitedCountries = this.getVisitedCountries(this.countryCodesMap);
|
||||||
console.log('Current visited countries:', visitedCountries);
|
console.log('Current visited countries:', visitedCountries);
|
||||||
|
|
||||||
if (visitedCountries.length === 0) {
|
if (visitedCountries.length === 0) {
|
||||||
console.log('No visited countries found');
|
console.log('No visited countries found');
|
||||||
return;
|
return;
|
||||||
|
|
@ -579,7 +579,7 @@ export default class extends BaseController {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
const endDate = urlParams.get('end_at') || new Date().toISOString();
|
const endDate = urlParams.get('end_at') || new Date().toISOString();
|
||||||
|
|
||||||
console.log('Fetching photos for date range:', { startDate, endDate });
|
console.log('Fetching photos for date range:', { startDate, endDate });
|
||||||
fetchAndDisplayPhotos({
|
fetchAndDisplayPhotos({
|
||||||
map: this.map,
|
map: this.map,
|
||||||
|
|
@ -632,6 +632,9 @@ export default class extends BaseController {
|
||||||
// Clear the visit circles when layer is disabled
|
// Clear the visit circles when layer is disabled
|
||||||
this.visitsManager.visitCircles.clearLayers();
|
this.visitsManager.visitCircles.clearLayers();
|
||||||
}
|
}
|
||||||
|
} else if (event.name === 'Fog of War') {
|
||||||
|
// Fog canvas will be automatically removed by the layer's onRemove method
|
||||||
|
this.fogOverlay = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -705,7 +708,7 @@ export default class extends BaseController {
|
||||||
Points: this.markersLayer || L.layerGroup(),
|
Points: this.markersLayer || L.layerGroup(),
|
||||||
Routes: this.polylinesLayer || L.layerGroup(),
|
Routes: this.polylinesLayer || L.layerGroup(),
|
||||||
Heatmap: this.heatmapLayer || L.layerGroup(),
|
Heatmap: this.heatmapLayer || L.layerGroup(),
|
||||||
"Fog of War": new this.fogOverlay(),
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
"Scratch map": this.scratchLayer || L.layerGroup(),
|
||||||
Areas: this.areasLayer || L.layerGroup(),
|
Areas: this.areasLayer || L.layerGroup(),
|
||||||
Photos: this.photoMarkers || L.layerGroup()
|
Photos: this.photoMarkers || L.layerGroup()
|
||||||
|
|
@ -1107,7 +1110,7 @@ export default class extends BaseController {
|
||||||
Routes: this.polylinesLayer || L.layerGroup(),
|
Routes: this.polylinesLayer || L.layerGroup(),
|
||||||
Tracks: this.tracksLayer || L.layerGroup(),
|
Tracks: this.tracksLayer || L.layerGroup(),
|
||||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||||
"Fog of War": new this.fogOverlay(),
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
"Scratch map": this.scratchLayer || L.layerGroup(),
|
||||||
Areas: this.areasLayer || L.layerGroup(),
|
Areas: this.areasLayer || L.layerGroup(),
|
||||||
Photos: this.photoMarkers || L.layerGroup()
|
Photos: this.photoMarkers || L.layerGroup()
|
||||||
|
|
@ -1361,7 +1364,7 @@ export default class extends BaseController {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
const startDate = urlParams.get('start_at') || new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
const endDate = urlParams.get('end_at') || new Date().toISOString();
|
const endDate = urlParams.get('end_at') || new Date().toISOString();
|
||||||
|
|
||||||
console.log('Auto-fetching photos for date range:', { startDate, endDate });
|
console.log('Auto-fetching photos for date range:', { startDate, endDate });
|
||||||
fetchAndDisplayPhotos({
|
fetchAndDisplayPhotos({
|
||||||
map: this.map,
|
map: this.map,
|
||||||
|
|
@ -1383,9 +1386,9 @@ export default class extends BaseController {
|
||||||
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
|
if (this.visitsManager && typeof this.visitsManager.fetchAndDisplayVisits === 'function') {
|
||||||
// Check if confirmed visits layer is enabled by default (it's added to map in constructor)
|
// Check if confirmed visits layer is enabled by default (it's added to map in constructor)
|
||||||
const confirmedVisitsEnabled = this.map.hasLayer(this.visitsManager.getConfirmedVisitCirclesLayer());
|
const confirmedVisitsEnabled = this.map.hasLayer(this.visitsManager.getConfirmedVisitCirclesLayer());
|
||||||
|
|
||||||
console.log('Visits initialization - confirmedVisitsEnabled:', confirmedVisitsEnabled);
|
console.log('Visits initialization - confirmedVisitsEnabled:', confirmedVisitsEnabled);
|
||||||
|
|
||||||
if (confirmedVisitsEnabled) {
|
if (confirmedVisitsEnabled) {
|
||||||
console.log('Confirmed visits layer enabled by default - fetching visits data');
|
console.log('Confirmed visits layer enabled by default - fetching visits data');
|
||||||
this.visitsManager.fetchAndDisplayVisits();
|
this.visitsManager.fetchAndDisplayVisits();
|
||||||
|
|
@ -1914,7 +1917,7 @@ export default class extends BaseController {
|
||||||
Routes: this.polylinesLayer || L.layerGroup(),
|
Routes: this.polylinesLayer || L.layerGroup(),
|
||||||
Tracks: this.tracksLayer || L.layerGroup(),
|
Tracks: this.tracksLayer || L.layerGroup(),
|
||||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||||
"Fog of War": new this.fogOverlay(),
|
"Fog of War": this.fogOverlay,
|
||||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
"Scratch map": this.scratchLayer || L.layerGroup(),
|
||||||
Areas: this.areasLayer || L.layerGroup(),
|
Areas: this.areasLayer || L.layerGroup(),
|
||||||
Photos: this.photoMarkers || L.layerGroup(),
|
Photos: this.photoMarkers || L.layerGroup(),
|
||||||
|
|
|
||||||
|
|
@ -104,24 +104,61 @@ function getMetersPerPixel(latitude, zoom) {
|
||||||
|
|
||||||
export function createFogOverlay() {
|
export function createFogOverlay() {
|
||||||
return L.Layer.extend({
|
return L.Layer.extend({
|
||||||
onAdd: (map) => {
|
onAdd: function(map) {
|
||||||
|
this._map = map;
|
||||||
|
|
||||||
|
// Initialize the fog canvas
|
||||||
initializeFogCanvas(map);
|
initializeFogCanvas(map);
|
||||||
|
|
||||||
|
// Get the map controller to access markers and settings
|
||||||
|
const mapElement = document.getElementById('map');
|
||||||
|
if (mapElement && mapElement._stimulus_controllers) {
|
||||||
|
const controller = mapElement._stimulus_controllers.find(c => c.identifier === 'maps');
|
||||||
|
if (controller) {
|
||||||
|
this._controller = controller;
|
||||||
|
|
||||||
|
// Draw initial fog if we have markers
|
||||||
|
if (controller.markers && controller.markers.length > 0) {
|
||||||
|
drawFogCanvas(map, controller.markers, controller.clearFogRadius, controller.fogLinethreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add resize event handlers to update fog size
|
// Add resize event handlers to update fog size
|
||||||
map.on('resize', () => {
|
this._onResize = () => {
|
||||||
// Set canvas size to match map container
|
const fog = document.getElementById('fog');
|
||||||
const mapSize = map.getSize();
|
if (fog) {
|
||||||
fog.width = mapSize.x;
|
const mapSize = map.getSize();
|
||||||
fog.height = mapSize.y;
|
fog.width = mapSize.x;
|
||||||
});
|
fog.height = mapSize.y;
|
||||||
|
|
||||||
|
// Redraw fog after resize
|
||||||
|
if (this._controller && this._controller.markers) {
|
||||||
|
drawFogCanvas(map, this._controller.markers, this._controller.clearFogRadius, this._controller.fogLinethreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
map.on('resize', this._onResize);
|
||||||
},
|
},
|
||||||
onRemove: (map) => {
|
|
||||||
|
onRemove: function(map) {
|
||||||
const fog = document.getElementById('fog');
|
const fog = document.getElementById('fog');
|
||||||
if (fog) {
|
if (fog) {
|
||||||
fog.remove();
|
fog.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up event listener
|
// Clean up event listener
|
||||||
map.off('resize');
|
if (this._onResize) {
|
||||||
|
map.off('resize', this._onResize);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Method to update fog when markers change
|
||||||
|
updateFog: function(markers, clearFogRadius, fogLinethreshold) {
|
||||||
|
if (this._map) {
|
||||||
|
drawFogCanvas(this._map, markers, clearFogRadius, fogLinethreshold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
819
e2e/map.spec.js
Normal file
819
e2e/map.spec.js
Normal file
|
|
@ -0,0 +1,819 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map functionality tests based on MAP_FUNCTIONALITY.md
|
||||||
|
* These tests cover the core features of the /map page
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe('Map Functionality', () => {
|
||||||
|
let page;
|
||||||
|
let context;
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
context = await browser.newContext();
|
||||||
|
page = await context.newPage();
|
||||||
|
|
||||||
|
// Sign in once for all tests
|
||||||
|
await page.goto('/users/sign_in');
|
||||||
|
await page.waitForSelector('input[name="user[email]"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
await page.fill('input[name="user[email]"]', 'demo@dawarich.app');
|
||||||
|
await page.fill('input[name="user[password]"]', 'password');
|
||||||
|
await page.click('input[type="submit"][value="Log in"]');
|
||||||
|
|
||||||
|
// Wait for redirect to map page
|
||||||
|
await page.waitForURL('/map', { timeout: 10000 });
|
||||||
|
await page.waitForSelector('#map', { timeout: 10000 });
|
||||||
|
await page.waitForSelector('.leaflet-container', { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await page.close();
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
// Just navigate to map page (already authenticated)
|
||||||
|
await page.goto('/map');
|
||||||
|
await page.waitForSelector('#map', { timeout: 10000 });
|
||||||
|
await page.waitForSelector('.leaflet-container', { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Core Map Display', () => {
|
||||||
|
test('should load the map page successfully', async () => {
|
||||||
|
await expect(page).toHaveTitle(/Map/);
|
||||||
|
await expect(page.locator('#map')).toBeVisible();
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display Leaflet map with default tiles', async () => {
|
||||||
|
// Check that the Leaflet map container is present
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
|
||||||
|
// Check for tile layers (using a more specific selector)
|
||||||
|
await expect(page.locator('.leaflet-pane.leaflet-tile-pane')).toBeAttached();
|
||||||
|
|
||||||
|
// Check for map controls
|
||||||
|
await expect(page.locator('.leaflet-control-zoom')).toBeVisible();
|
||||||
|
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have scale control visible', async () => {
|
||||||
|
await expect(page.locator('.leaflet-control-scale')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display stats control with distance and points', async () => {
|
||||||
|
await expect(page.locator('.leaflet-control-stats')).toBeVisible();
|
||||||
|
|
||||||
|
const statsText = await page.locator('.leaflet-control-stats').textContent();
|
||||||
|
expect(statsText).toMatch(/\d+\s+(km|mi)\s+\|\s+\d+\s+points/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Date and Time Navigation', () => {
|
||||||
|
test('should display date navigation controls', async () => {
|
||||||
|
// Check for date inputs
|
||||||
|
await expect(page.locator('input#start_at')).toBeVisible();
|
||||||
|
await expect(page.locator('input#end_at')).toBeVisible();
|
||||||
|
|
||||||
|
// Check for navigation arrows
|
||||||
|
await expect(page.locator('a:has-text("◀️")')).toBeVisible();
|
||||||
|
await expect(page.locator('a:has-text("▶️")')).toBeVisible();
|
||||||
|
|
||||||
|
// Check for quick access buttons
|
||||||
|
await expect(page.locator('a:has-text("Today")')).toBeVisible();
|
||||||
|
await expect(page.locator('a:has-text("Last 7 days")')).toBeVisible();
|
||||||
|
await expect(page.locator('a:has-text("Last month")')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow changing date range', async () => {
|
||||||
|
const startDateInput = page.locator('input#start_at');
|
||||||
|
|
||||||
|
// Change start date
|
||||||
|
const newStartDate = '2024-01-01T00:00';
|
||||||
|
await startDateInput.fill(newStartDate);
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
await page.locator('input[type="submit"][value="Search"]').click();
|
||||||
|
|
||||||
|
// Wait for page to load
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Check that URL parameters were updated
|
||||||
|
const url = page.url();
|
||||||
|
expect(url).toContain('start_at=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to today when clicking Today button', async () => {
|
||||||
|
await page.locator('a:has-text("Today")').click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const url = page.url();
|
||||||
|
// Allow for timezone differences by checking for current date or next day
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
|
expect(url.includes(today) || url.includes(tomorrow)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Map Layer Controls', () => {
|
||||||
|
test('should have layer control panel', async () => {
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await expect(layerControl).toBeVisible();
|
||||||
|
|
||||||
|
// Click to expand if collapsed
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
// Check for base layer options
|
||||||
|
await expect(page.locator('.leaflet-control-layers-base')).toBeVisible();
|
||||||
|
|
||||||
|
// Check for overlay options
|
||||||
|
await expect(page.locator('.leaflet-control-layers-overlays')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow toggling overlay layers', async () => {
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
// Find the Points layer checkbox specifically
|
||||||
|
const pointsCheckbox = page.locator('.leaflet-control-layers-overlays').locator('label:has-text("Points")').locator('input');
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
const initialState = await pointsCheckbox.isChecked();
|
||||||
|
|
||||||
|
if (initialState) {
|
||||||
|
// If points are initially visible, verify they exist, then hide them
|
||||||
|
const initialPointsCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
|
||||||
|
|
||||||
|
// Toggle off
|
||||||
|
await pointsCheckbox.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify points are hidden
|
||||||
|
const afterHideCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
|
||||||
|
expect(afterHideCount).toBe(0);
|
||||||
|
|
||||||
|
// Toggle back on
|
||||||
|
await pointsCheckbox.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify points are visible again
|
||||||
|
const afterShowCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
|
||||||
|
expect(afterShowCount).toBe(initialPointsCount);
|
||||||
|
} else {
|
||||||
|
// If points are initially hidden, show them first
|
||||||
|
await pointsCheckbox.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify points are now visible
|
||||||
|
const pointsCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
|
||||||
|
expect(pointsCount).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Toggle back off
|
||||||
|
await pointsCheckbox.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Verify points are hidden again
|
||||||
|
const finalCount = await page.locator('.leaflet-marker-pane .leaflet-marker-icon').count();
|
||||||
|
expect(finalCount).toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure checkbox state matches what we expect
|
||||||
|
const finalState = await pointsCheckbox.isChecked();
|
||||||
|
expect(finalState).toBe(initialState);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should switch between base map layers', async () => {
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
// Find base layer radio buttons
|
||||||
|
const baseLayerRadios = page.locator('.leaflet-control-layers-base input[type="radio"]');
|
||||||
|
const secondRadio = baseLayerRadios.nth(1);
|
||||||
|
|
||||||
|
if (await secondRadio.isVisible()) {
|
||||||
|
await secondRadio.check();
|
||||||
|
await page.waitForTimeout(1000); // Wait for tiles to load
|
||||||
|
|
||||||
|
await expect(secondRadio).toBeChecked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Settings Panel', () => {
|
||||||
|
test('should open and close settings panel', async () => {
|
||||||
|
// Find and click settings button (gear icon)
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await expect(settingsButton).toBeVisible();
|
||||||
|
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
// Check that settings panel is visible
|
||||||
|
await expect(page.locator('.leaflet-settings-panel')).toBeVisible();
|
||||||
|
await expect(page.locator('#settings-form')).toBeVisible();
|
||||||
|
|
||||||
|
// Close settings panel
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
// Settings panel should be hidden
|
||||||
|
await expect(page.locator('.leaflet-settings-panel')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow adjusting route opacity', async () => {
|
||||||
|
// First ensure routes are visible
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
const routesCheckbox = page.locator('.leaflet-control-layers-overlays').locator('label:has-text("Routes")').locator('input');
|
||||||
|
if (await routesCheckbox.isVisible() && !(await routesCheckbox.isChecked())) {
|
||||||
|
await routesCheckbox.check();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if routes exist before testing opacity
|
||||||
|
const routesExist = await page.locator('.leaflet-overlay-pane svg path').count() > 0;
|
||||||
|
|
||||||
|
if (routesExist) {
|
||||||
|
// Get initial opacity of routes before changing
|
||||||
|
const initialOpacity = await page.locator('.leaflet-overlay-pane svg path').first().evaluate(el => {
|
||||||
|
return window.getComputedStyle(el).opacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
const opacityInput = page.locator('#route-opacity');
|
||||||
|
await expect(opacityInput).toBeVisible();
|
||||||
|
|
||||||
|
// Change opacity value to 30%
|
||||||
|
await opacityInput.fill('30');
|
||||||
|
|
||||||
|
// Submit settings
|
||||||
|
await page.locator('#settings-form button[type="submit"]').click();
|
||||||
|
|
||||||
|
// Wait for settings to be applied
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check that the route opacity actually changed
|
||||||
|
const newOpacity = await page.locator('.leaflet-overlay-pane svg path').first().evaluate(el => {
|
||||||
|
return window.getComputedStyle(el).opacity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// The new opacity should be approximately 0.3 (30%)
|
||||||
|
const numericOpacity = parseFloat(newOpacity);
|
||||||
|
expect(numericOpacity).toBeCloseTo(0.3, 1);
|
||||||
|
expect(numericOpacity).not.toBe(parseFloat(initialOpacity));
|
||||||
|
} else {
|
||||||
|
// If no routes exist, just verify the settings can be changed
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
const opacityInput = page.locator('#route-opacity');
|
||||||
|
await expect(opacityInput).toBeVisible();
|
||||||
|
|
||||||
|
await opacityInput.fill('30');
|
||||||
|
await page.locator('#settings-form button[type="submit"]').click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify the setting was persisted by reopening panel
|
||||||
|
await settingsButton.click();
|
||||||
|
await expect(page.locator('#route-opacity')).toHaveValue('30');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow configuring fog of war settings', async () => {
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
const fogRadiusInput = page.locator('#fog_of_war_meters');
|
||||||
|
await expect(fogRadiusInput).toBeVisible();
|
||||||
|
|
||||||
|
// Change values
|
||||||
|
await fogRadiusInput.fill('100');
|
||||||
|
|
||||||
|
const fogThresholdInput = page.locator('#fog_of_war_threshold');
|
||||||
|
await expect(fogThresholdInput).toBeVisible();
|
||||||
|
|
||||||
|
await fogThresholdInput.fill('120');
|
||||||
|
|
||||||
|
// Verify values were set
|
||||||
|
await expect(fogRadiusInput).toHaveValue('100');
|
||||||
|
await expect(fogThresholdInput).toHaveValue('120');
|
||||||
|
|
||||||
|
// Submit settings
|
||||||
|
await page.locator('#settings-form button[type="submit"]').click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify settings were applied by reopening panel and checking values
|
||||||
|
await settingsButton.click();
|
||||||
|
await expect(page.locator('#fog_of_war_meters')).toHaveValue('100');
|
||||||
|
await expect(page.locator('#fog_of_war_threshold')).toHaveValue('120');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should enable fog of war and verify it works', async () => {
|
||||||
|
// First, enable the Fog of War layer
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
// Wait for layer control to be fully expanded
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Find and enable the Fog of War layer checkbox
|
||||||
|
// Try multiple approaches to find the Fog of War checkbox
|
||||||
|
let fogCheckbox = page.locator('.leaflet-control-layers-overlays').locator('label:has-text("Fog of War")').locator('input');
|
||||||
|
|
||||||
|
// Alternative approach if first one doesn't work
|
||||||
|
if (!(await fogCheckbox.isVisible())) {
|
||||||
|
fogCheckbox = page.locator('.leaflet-control-layers-overlays').locator('input').filter({
|
||||||
|
has: page.locator(':text("Fog of War")')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another fallback approach
|
||||||
|
if (!(await fogCheckbox.isVisible())) {
|
||||||
|
// Look for any checkbox followed by text containing "Fog of War"
|
||||||
|
const allCheckboxes = page.locator('.leaflet-control-layers-overlays input[type="checkbox"]');
|
||||||
|
const count = await allCheckboxes.count();
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const checkbox = allCheckboxes.nth(i);
|
||||||
|
const nextSibling = checkbox.locator('+ span');
|
||||||
|
if (await nextSibling.isVisible() && (await nextSibling.textContent())?.includes('Fog of War')) {
|
||||||
|
fogCheckbox = checkbox;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await fogCheckbox.isVisible()) {
|
||||||
|
// Check initial state
|
||||||
|
const initiallyChecked = await fogCheckbox.isChecked();
|
||||||
|
|
||||||
|
// Enable fog of war if not already enabled
|
||||||
|
if (!initiallyChecked) {
|
||||||
|
await fogCheckbox.check();
|
||||||
|
await page.waitForTimeout(2000); // Wait for fog canvas to be created
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that fog canvas is created and attached to the map
|
||||||
|
await expect(page.locator('#fog')).toBeAttached();
|
||||||
|
|
||||||
|
// Verify the fog canvas has the correct properties
|
||||||
|
const fogCanvas = page.locator('#fog');
|
||||||
|
await expect(fogCanvas).toHaveAttribute('id', 'fog');
|
||||||
|
|
||||||
|
// Check that the canvas has non-zero dimensions (indicating it's been sized)
|
||||||
|
const canvasBox = await fogCanvas.boundingBox();
|
||||||
|
expect(canvasBox?.width).toBeGreaterThan(0);
|
||||||
|
expect(canvasBox?.height).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify canvas styling indicates it's positioned correctly
|
||||||
|
const canvasStyle = await fogCanvas.evaluate(el => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
return {
|
||||||
|
position: style.position,
|
||||||
|
zIndex: style.zIndex,
|
||||||
|
pointerEvents: style.pointerEvents
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(canvasStyle.position).toBe('absolute');
|
||||||
|
expect(canvasStyle.zIndex).toBe('400');
|
||||||
|
expect(canvasStyle.pointerEvents).toBe('none');
|
||||||
|
|
||||||
|
// Test disabling fog of war
|
||||||
|
await fogCheckbox.uncheck();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Fog canvas should be removed when layer is disabled
|
||||||
|
await expect(page.locator('#fog')).not.toBeAttached();
|
||||||
|
|
||||||
|
// Re-enable to test toggle functionality
|
||||||
|
await fogCheckbox.check();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should be back
|
||||||
|
await expect(page.locator('#fog')).toBeAttached();
|
||||||
|
} else {
|
||||||
|
// If fog layer checkbox is not found, skip fog testing but verify layer control works
|
||||||
|
await expect(page.locator('.leaflet-control-layers-overlays')).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should toggle points rendering mode', async () => {
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await settingsButton.click();
|
||||||
|
|
||||||
|
const rawModeRadio = page.locator('#raw');
|
||||||
|
const simplifiedModeRadio = page.locator('#simplified');
|
||||||
|
|
||||||
|
await expect(rawModeRadio).toBeVisible();
|
||||||
|
await expect(simplifiedModeRadio).toBeVisible();
|
||||||
|
|
||||||
|
// Get initial mode
|
||||||
|
const initiallyRaw = await rawModeRadio.isChecked();
|
||||||
|
|
||||||
|
// Test toggling between modes
|
||||||
|
if (initiallyRaw) {
|
||||||
|
// Switch to simplified mode
|
||||||
|
await simplifiedModeRadio.check();
|
||||||
|
await expect(simplifiedModeRadio).toBeChecked();
|
||||||
|
await expect(rawModeRadio).not.toBeChecked();
|
||||||
|
} else {
|
||||||
|
// Switch to raw mode
|
||||||
|
await rawModeRadio.check();
|
||||||
|
await expect(rawModeRadio).toBeChecked();
|
||||||
|
await expect(simplifiedModeRadio).not.toBeChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit settings
|
||||||
|
await page.locator('#settings-form button[type="submit"]').click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify settings were applied by reopening panel and checking selection persisted
|
||||||
|
await settingsButton.click();
|
||||||
|
if (initiallyRaw) {
|
||||||
|
await expect(page.locator('#simplified')).toBeChecked();
|
||||||
|
} else {
|
||||||
|
await expect(page.locator('#raw')).toBeChecked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Calendar Panel', () => {
|
||||||
|
test('should open and close calendar panel', async () => {
|
||||||
|
// Find and click calendar button
|
||||||
|
const calendarButton = page.locator('.toggle-panel-button');
|
||||||
|
await expect(calendarButton).toBeVisible();
|
||||||
|
await expect(calendarButton).toHaveText('📅');
|
||||||
|
|
||||||
|
// Get initial panel state (should be hidden)
|
||||||
|
const panel = page.locator('.leaflet-right-panel');
|
||||||
|
const initiallyVisible = await panel.isVisible();
|
||||||
|
|
||||||
|
await calendarButton.click();
|
||||||
|
await page.waitForTimeout(1000); // Wait for panel animation
|
||||||
|
|
||||||
|
// Check that calendar panel state changed
|
||||||
|
await expect(panel).toBeAttached();
|
||||||
|
const afterClickVisible = await panel.isVisible();
|
||||||
|
expect(afterClickVisible).not.toBe(initiallyVisible);
|
||||||
|
|
||||||
|
// Close panel
|
||||||
|
await calendarButton.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Panel should return to initial state
|
||||||
|
const finalVisible = await panel.isVisible();
|
||||||
|
expect(finalVisible).toBe(initiallyVisible);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display year selection and months grid', async () => {
|
||||||
|
const calendarButton = page.locator('.toggle-panel-button');
|
||||||
|
await calendarButton.click();
|
||||||
|
await page.waitForTimeout(1000); // Wait for panel animation
|
||||||
|
|
||||||
|
// Verify panel is now visible
|
||||||
|
const panel = page.locator('.leaflet-right-panel');
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
|
||||||
|
// Check year selector - may be hidden but attached
|
||||||
|
await expect(page.locator('#year-select')).toBeAttached();
|
||||||
|
|
||||||
|
// Check months grid - may be hidden but attached
|
||||||
|
await expect(page.locator('#months-grid')).toBeAttached();
|
||||||
|
|
||||||
|
// Check that there are month buttons
|
||||||
|
const monthButtons = page.locator('#months-grid a.btn');
|
||||||
|
const monthCount = await monthButtons.count();
|
||||||
|
expect(monthCount).toBeGreaterThan(0);
|
||||||
|
expect(monthCount).toBeLessThanOrEqual(12); // Should not exceed 12 months
|
||||||
|
|
||||||
|
// Check whole year link - may be hidden but attached
|
||||||
|
await expect(page.locator('#whole-year-link')).toBeAttached();
|
||||||
|
|
||||||
|
// Verify at least one month button is clickable
|
||||||
|
if (monthCount > 0) {
|
||||||
|
const firstMonth = monthButtons.first();
|
||||||
|
await expect(firstMonth).toHaveAttribute('href');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display visited cities section', async () => {
|
||||||
|
const calendarButton = page.locator('.toggle-panel-button');
|
||||||
|
await calendarButton.click();
|
||||||
|
await page.waitForTimeout(1000); // Wait for panel animation
|
||||||
|
|
||||||
|
// Verify panel is open
|
||||||
|
await expect(page.locator('.leaflet-right-panel')).toBeVisible();
|
||||||
|
|
||||||
|
// Check visited cities container
|
||||||
|
const citiesContainer = page.locator('#visited-cities-container');
|
||||||
|
await expect(citiesContainer).toBeAttached();
|
||||||
|
|
||||||
|
// Check visited cities list
|
||||||
|
const citiesList = page.locator('#visited-cities-list');
|
||||||
|
await expect(citiesList).toBeAttached();
|
||||||
|
|
||||||
|
// The cities list might be empty or populated depending on test data
|
||||||
|
// At minimum, verify the structure is there for cities to be displayed
|
||||||
|
const listExists = await citiesList.isVisible();
|
||||||
|
if (listExists) {
|
||||||
|
// If list is visible, it should be a proper container for city data
|
||||||
|
expect(await citiesList.getAttribute('id')).toBe('visited-cities-list');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Visits System', () => {
|
||||||
|
test('should have visits drawer button', async () => {
|
||||||
|
const visitsButton = page.locator('.drawer-button');
|
||||||
|
await expect(visitsButton).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open and close visits drawer', async () => {
|
||||||
|
const visitsButton = page.locator('.drawer-button');
|
||||||
|
await visitsButton.click();
|
||||||
|
|
||||||
|
// Check that visits drawer opens
|
||||||
|
await expect(page.locator('#visits-drawer')).toBeVisible();
|
||||||
|
await expect(page.locator('#visits-list')).toBeVisible();
|
||||||
|
|
||||||
|
// Close drawer
|
||||||
|
await visitsButton.click();
|
||||||
|
|
||||||
|
// Drawer should slide closed (but element might still be in DOM)
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should have area selection tool button', async () => {
|
||||||
|
const selectionButton = page.locator('#selection-tool-button');
|
||||||
|
await expect(selectionButton).toBeVisible();
|
||||||
|
await expect(selectionButton).toHaveText('⚓️');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should activate selection mode', async () => {
|
||||||
|
const selectionButton = page.locator('#selection-tool-button');
|
||||||
|
await selectionButton.click();
|
||||||
|
|
||||||
|
// Button should become active
|
||||||
|
await expect(selectionButton).toHaveClass(/active/);
|
||||||
|
|
||||||
|
// Click again to deactivate
|
||||||
|
await selectionButton.click();
|
||||||
|
|
||||||
|
// Button should no longer be active
|
||||||
|
await expect(selectionButton).not.toHaveClass(/active/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Interactive Map Elements', () => {
|
||||||
|
test('should allow map dragging and zooming', async () => {
|
||||||
|
const mapContainer = page.locator('.leaflet-container');
|
||||||
|
|
||||||
|
// Get initial zoom level
|
||||||
|
const initialZoomButton = page.locator('.leaflet-control-zoom-in');
|
||||||
|
await expect(initialZoomButton).toBeVisible();
|
||||||
|
|
||||||
|
// Zoom in
|
||||||
|
await initialZoomButton.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Zoom out
|
||||||
|
const zoomOutButton = page.locator('.leaflet-control-zoom-out');
|
||||||
|
await zoomOutButton.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Test map dragging
|
||||||
|
await mapContainer.hover();
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(100, 100);
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display markers if data is available', async () => {
|
||||||
|
// Check if there are any markers on the map
|
||||||
|
const markers = page.locator('.leaflet-marker-pane .leaflet-marker-icon');
|
||||||
|
|
||||||
|
// If markers exist, test their functionality
|
||||||
|
if (await markers.first().isVisible()) {
|
||||||
|
await expect(markers.first()).toBeVisible();
|
||||||
|
|
||||||
|
// Test marker click (should open popup)
|
||||||
|
await markers.first().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Check if popup appeared
|
||||||
|
const popup = page.locator('.leaflet-popup');
|
||||||
|
await expect(popup).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display routes/polylines if data is available', async () => {
|
||||||
|
// Check if there are any polylines on the map
|
||||||
|
const polylines = page.locator('.leaflet-overlay-pane svg path');
|
||||||
|
|
||||||
|
if (await polylines.first().isVisible()) {
|
||||||
|
await expect(polylines.first()).toBeVisible();
|
||||||
|
|
||||||
|
// Test polyline hover
|
||||||
|
await polylines.first().hover();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Areas Management', () => {
|
||||||
|
test('should have draw control when areas layer is active', async () => {
|
||||||
|
// Open layer control
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await layerControl.click();
|
||||||
|
|
||||||
|
// Find and enable Areas layer
|
||||||
|
const areasCheckbox = page.locator('.leaflet-control-layers-overlays').locator('input').filter({ hasText: /Areas/ }).first();
|
||||||
|
|
||||||
|
if (await areasCheckbox.isVisible()) {
|
||||||
|
await areasCheckbox.check();
|
||||||
|
|
||||||
|
// Check for draw control
|
||||||
|
await expect(page.locator('.leaflet-draw')).toBeVisible();
|
||||||
|
|
||||||
|
// Check for circle draw tool
|
||||||
|
await expect(page.locator('.leaflet-draw-draw-circle')).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Performance and Loading', () => {
|
||||||
|
test('should load within reasonable time', async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
await page.goto('/map');
|
||||||
|
await page.waitForSelector('.leaflet-container', { timeout: 15000 });
|
||||||
|
|
||||||
|
const loadTime = Date.now() - startTime;
|
||||||
|
expect(loadTime).toBeLessThan(15000); // Should load within 15 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle network errors gracefully', async () => {
|
||||||
|
// Should still show the page structure even if tiles don't load
|
||||||
|
await expect(page.locator('#map')).toBeVisible();
|
||||||
|
|
||||||
|
// Test with offline network after initial load
|
||||||
|
await page.context().setOffline(true);
|
||||||
|
|
||||||
|
// Page should still be functional even when offline
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
|
||||||
|
// Restore network
|
||||||
|
await page.context().setOffline(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Responsive Design', () => {
|
||||||
|
test('should adapt to mobile viewport', async () => {
|
||||||
|
// Set mobile viewport
|
||||||
|
await page.setViewportSize({ width: 375, height: 667 });
|
||||||
|
|
||||||
|
await page.goto('/map');
|
||||||
|
await page.waitForSelector('.leaflet-container');
|
||||||
|
|
||||||
|
// Map should still be visible and functional
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
await expect(page.locator('.leaflet-control-zoom')).toBeVisible();
|
||||||
|
|
||||||
|
// Date controls should be responsive
|
||||||
|
await expect(page.locator('input#start_at')).toBeVisible();
|
||||||
|
await expect(page.locator('input#end_at')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work on tablet viewport', async () => {
|
||||||
|
// Set tablet viewport
|
||||||
|
await page.setViewportSize({ width: 768, height: 1024 });
|
||||||
|
|
||||||
|
await page.goto('/map');
|
||||||
|
await page.waitForSelector('.leaflet-container');
|
||||||
|
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
await expect(page.locator('.leaflet-control-layers')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Accessibility', () => {
|
||||||
|
test('should have proper accessibility attributes', async () => {
|
||||||
|
// Check for map container accessibility
|
||||||
|
const mapContainer = page.locator('#map');
|
||||||
|
await expect(mapContainer).toHaveAttribute('data-controller', 'maps points');
|
||||||
|
|
||||||
|
// Check form labels
|
||||||
|
await expect(page.locator('label[for="start_at"]')).toBeVisible();
|
||||||
|
await expect(page.locator('label[for="end_at"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Check button accessibility
|
||||||
|
const searchButton = page.locator('input[type="submit"][value="Search"]');
|
||||||
|
await expect(searchButton).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support keyboard navigation', async () => {
|
||||||
|
// Test tab navigation through form elements
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
// Should be able to focus on interactive elements
|
||||||
|
const focusedElement = page.locator(':focus');
|
||||||
|
await expect(focusedElement).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Data Integration', () => {
|
||||||
|
test('should handle empty data state', async () => {
|
||||||
|
// Navigate to a date range with no data
|
||||||
|
await page.goto('/map?start_at=1990-01-01T00:00&end_at=1990-01-02T00:00');
|
||||||
|
await page.waitForSelector('.leaflet-container');
|
||||||
|
|
||||||
|
// Map should still load
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
|
||||||
|
// Stats should show zero
|
||||||
|
const statsControl = page.locator('.leaflet-control-stats');
|
||||||
|
if (await statsControl.isVisible()) {
|
||||||
|
const statsText = await statsControl.textContent();
|
||||||
|
expect(statsText).toContain('0');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update URL parameters when navigating', async () => {
|
||||||
|
const initialUrl = page.url();
|
||||||
|
|
||||||
|
// Click on a navigation arrow
|
||||||
|
await page.locator('a:has-text("▶️")').click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
const newUrl = page.url();
|
||||||
|
expect(newUrl).not.toBe(initialUrl);
|
||||||
|
expect(newUrl).toContain('start_at=');
|
||||||
|
expect(newUrl).toContain('end_at=');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Error Handling', () => {
|
||||||
|
test('should display error messages for invalid date ranges', async () => {
|
||||||
|
// Get initial URL to compare after invalid date submission
|
||||||
|
const initialUrl = page.url();
|
||||||
|
|
||||||
|
// Try to set end date before start date
|
||||||
|
await page.locator('input#start_at').fill('2024-12-31T23:59');
|
||||||
|
await page.locator('input#end_at').fill('2024-01-01T00:00');
|
||||||
|
|
||||||
|
await page.locator('input[type="submit"][value="Search"]').click();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Should handle gracefully (either show error or correct the dates)
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
|
||||||
|
// Verify that either:
|
||||||
|
// 1. An error message is shown, OR
|
||||||
|
// 2. The dates were automatically corrected, OR
|
||||||
|
// 3. The URL reflects the corrected date range
|
||||||
|
const finalUrl = page.url();
|
||||||
|
const hasErrorMessage = await page.locator('.alert, .error, [class*="error"]').count() > 0;
|
||||||
|
const urlChanged = finalUrl !== initialUrl;
|
||||||
|
|
||||||
|
// At least one of these should be true - either error shown or dates handled
|
||||||
|
expect(hasErrorMessage || urlChanged).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle JavaScript errors gracefully', async () => {
|
||||||
|
// Listen for console errors
|
||||||
|
const consoleErrors = [];
|
||||||
|
page.on('console', message => {
|
||||||
|
if (message.type() === 'error') {
|
||||||
|
consoleErrors.push(message.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/map');
|
||||||
|
await page.waitForSelector('.leaflet-container');
|
||||||
|
|
||||||
|
// Map should still function despite any minor JS errors
|
||||||
|
await expect(page.locator('.leaflet-container')).toBeVisible();
|
||||||
|
|
||||||
|
// Critical functionality should work
|
||||||
|
const layerControl = page.locator('.leaflet-control-layers');
|
||||||
|
await expect(layerControl).toBeVisible();
|
||||||
|
|
||||||
|
// Settings button should be functional
|
||||||
|
const settingsButton = page.locator('.map-settings-button');
|
||||||
|
await expect(settingsButton).toBeVisible();
|
||||||
|
|
||||||
|
// Calendar button should be functional
|
||||||
|
const calendarButton = page.locator('.toggle-panel-button');
|
||||||
|
await expect(calendarButton).toBeVisible();
|
||||||
|
|
||||||
|
// Test that a basic interaction still works
|
||||||
|
await layerControl.click();
|
||||||
|
await expect(page.locator('.leaflet-control-layers-list')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
51
playwright.config.js
Normal file
51
playwright.config.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://playwright.dev/docs/test-configuration
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: [
|
||||||
|
['html'],
|
||||||
|
['junit', { outputFile: 'test-results/results.xml' }]
|
||||||
|
],
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
|
||||||
|
/* Take screenshot on failure */
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
|
||||||
|
/* Record video on failure */
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: 'RAILS_ENV=test rails server -p 3000',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue