dawarich/e2e/map/map-places-creation.spec.js
Evgenii Burmakin b1393ee674
0.36.0 (#1952)
* Implement OmniAuth GitHub authentication

* Fix omniauth GitHub scope to include user email access

* Remove margin-bottom

* Implement Google OAuth2 authentication

* Implement OIDC authentication for Dawarich using omniauth_openid_connect gem.

* Add patreon account linking and patron checking service

* Update docker-compose.yml to use boolean values instead of strings

* Add support for KML files

* Add tests

* Update changelog

* Remove patreon OAuth integration

* Move omniauthable to a concern

* Update an icon in integrations

* Update changelog

* Update app version

* Fix family location sharing toggle

* Move family location sharing to its own controller

* Update changelog

* Implement basic tagging functionality for places, allowing users to categorize and label places with custom tags.

* Add places management API and tags feature

* Add some changes related to places management feature

* Fix some tests

* Fix sometests

* Add places layer

* Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control

* Rework tag form

* Add hashtag

* Add privacy zones to tags

* Add notes to places and manage place tags

* Update changelog

* Update e2e tests

* Extract tag serializer to its own file

* Fix some tests

* Fix tags request specs

* Fix some tests

* Fix rest of the tests

* Revert some changes

* Add missing specs

* Revert changes in place export/import code

* Fix some specs

* Fix PlaceFinder to only consider global places when finding existing places

* Fix few more specs

* Fix visits creator spec

* Fix last tests

* Update place creating modal

* Add home location based on "Home" tagged place

* Save enabled tag layers

* Some fixes

* Fix bug where enabling place tag layers would trigger saving enabled layers, overwriting with incomplete data

* Update migration to use disable_ddl_transaction! and add up/down methods

* Fix tag layers restoration and filtering logic

* Update OIDC auto-registration and email/password registration settings

* Fix potential xss
2025-11-24 19:45:09 +01:00

334 lines
12 KiB
JavaScript

import { test, expect } from '@playwright/test';
import { navigateToMap } from '../helpers/navigation.js';
import { waitForMap } from '../helpers/map.js';
test.describe('Places Creation', () => {
test.beforeEach(async ({ page }) => {
await navigateToMap(page);
await waitForMap(page);
});
test('should enable place creation mode when "Create a place" button is clicked', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Verify button exists
await expect(createPlaceBtn).toBeVisible();
// Click to enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Verify creation mode is enabled
const isCreationMode = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.placesManager?.creationMode === true;
});
expect(isCreationMode).toBe(true);
});
test('should change button icon to X when in place creation mode', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Click to enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Verify button tooltip changed
const tooltip = await createPlaceBtn.getAttribute('data-tip');
expect(tooltip).toContain('click to cancel');
// Verify button has active state
const hasActiveClass = await createPlaceBtn.evaluate((btn) => {
return btn.classList.contains('active') ||
btn.style.backgroundColor !== '' ||
btn.hasAttribute('data-active');
});
expect(hasActiveClass).toBe(true);
});
test('should exit place creation mode when X button is clicked', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click again to disable
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Verify creation mode is disabled
const isCreationMode = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.placesManager?.creationMode === true;
});
expect(isCreationMode).toBe(false);
});
test('should open place creation popup when map is clicked in creation mode', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Get map container and click on it
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Verify modal is open
const modalOpen = await page.locator('[data-place-creation-target="modal"]').evaluate((modal) => {
return modal.classList.contains('modal-open');
});
expect(modalOpen).toBe(true);
// Verify form fields exist (latitude/longitude are hidden inputs, so we check they exist, not visibility)
await expect(page.locator('[data-place-creation-target="nameInput"]')).toBeVisible();
await expect(page.locator('[data-place-creation-target="latitudeInput"]')).toBeAttached();
await expect(page.locator('[data-place-creation-target="longitudeInput"]')).toBeAttached();
});
test('should allow user to provide name, notes and select tags in creation popup', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click on map
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Fill in the form
const nameInput = page.locator('[data-place-creation-target="nameInput"]');
await nameInput.fill('Test Place');
const noteInput = page.locator('textarea[name="note"]');
if (await noteInput.isVisible()) {
await noteInput.fill('This is a test note');
}
// Check if there are any tag checkboxes to select
const tagCheckboxes = page.locator('input[name="tag_ids[]"]');
const tagCount = await tagCheckboxes.count();
if (tagCount > 0) {
await tagCheckboxes.first().check();
}
// Verify fields are filled
await expect(nameInput).toHaveValue('Test Place');
});
test('should save place when Save button is clicked @destructive', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click on map
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Fill in the form with a unique name
const placeName = `E2E Test Place ${Date.now()}`;
const nameInput = page.locator('[data-place-creation-target="nameInput"]');
await nameInput.fill(placeName);
// Submit form
const submitBtn = page.locator('[data-place-creation-target="form"] button[type="submit"]');
// Set up a promise to wait for the place:created event
const placeCreatedPromise = page.evaluate(() => {
return new Promise((resolve) => {
document.addEventListener('place:created', (e) => {
resolve(e.detail);
}, { once: true });
});
});
await submitBtn.click();
// Wait for the place to be created
await placeCreatedPromise;
// Verify modal is closed
await page.waitForTimeout(500);
const modalOpen = await page.locator('[data-place-creation-target="modal"]').evaluate((modal) => {
return modal.classList.contains('modal-open');
});
expect(modalOpen).toBe(false);
// Verify success message is shown
const hasSuccessMessage = await page.evaluate(() => {
const flashMessages = document.querySelectorAll('.alert, .flash, [role="alert"]');
return Array.from(flashMessages).some(msg =>
msg.textContent.includes('success') ||
msg.classList.contains('alert-success')
);
});
expect(hasSuccessMessage).toBe(true);
});
test('should put clickable marker on map after saving place @destructive', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click on map
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Fill and submit form
const placeName = `E2E Test Place ${Date.now()}`;
await page.locator('[data-place-creation-target="nameInput"]').fill(placeName);
const placeCreatedPromise = page.evaluate(() => {
return new Promise((resolve) => {
document.addEventListener('place:created', (e) => {
resolve(e.detail);
}, { once: true });
});
});
await page.locator('[data-place-creation-target="form"] button[type="submit"]').click();
await placeCreatedPromise;
await page.waitForTimeout(1000);
// Verify marker was added to the map
const hasMarker = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
const placesLayer = controller?.placesManager?.placesLayer;
if (!placesLayer || !placesLayer._layers) {
return false;
}
return Object.keys(placesLayer._layers).length > 0;
});
expect(hasMarker).toBe(true);
});
test('should close popup and remove marker when Cancel is clicked', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click on map
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Check if creation marker exists
const hasCreationMarkerBefore = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.placesManager?.creationMarker !== null;
});
expect(hasCreationMarkerBefore).toBe(true);
// Click cancel
const cancelBtn = page.locator('[data-place-creation-target="modal"] button').filter({ hasText: /cancel|close/i }).first();
await cancelBtn.click();
await page.waitForTimeout(500);
// Verify modal is closed
const modalOpen = await page.locator('[data-place-creation-target="modal"]').evaluate((modal) => {
return modal.classList.contains('modal-open');
});
expect(modalOpen).toBe(false);
// Verify creation marker is removed
const hasCreationMarkerAfter = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
return controller?.placesManager?.creationMarker !== null;
});
expect(hasCreationMarkerAfter).toBe(false);
});
test('should close previous popup and open new one when clicking different location', async ({ page }) => {
const createPlaceBtn = page.locator('#create-place-btn');
// Enable creation mode
await createPlaceBtn.click();
await page.waitForTimeout(300);
// Click first location
const mapContainer = page.locator('#map');
await mapContainer.click({ position: { x: 300, y: 300 } });
await page.waitForTimeout(500);
// Get first coordinates
const firstCoords = await page.evaluate(() => {
const latInput = document.querySelector('[data-place-creation-target="latitudeInput"]');
const lngInput = document.querySelector('[data-place-creation-target="longitudeInput"]');
return {
lat: latInput?.value,
lng: lngInput?.value
};
});
// Verify first coordinates exist
expect(firstCoords.lat).toBeTruthy();
expect(firstCoords.lng).toBeTruthy();
// Use programmatic click to simulate clicking on a different map location
// This bypasses UI interference with modal
const secondCoords = await page.evaluate(() => {
const controller = window.Stimulus?.controllers.find(c => c.identifier === 'maps');
if (controller && controller.placesManager && controller.placesManager.creationMode) {
// Simulate clicking at a different location
const map = controller.map;
const center = map.getCenter();
const newLatlng = { lat: center.lat + 0.01, lng: center.lng + 0.01 };
// Trigger place creation at new location
controller.placesManager.handleMapClick({ latlng: newLatlng });
// Wait for UI update
return new Promise(resolve => {
setTimeout(() => {
const latInput = document.querySelector('[data-place-creation-target="latitudeInput"]');
const lngInput = document.querySelector('[data-place-creation-target="longitudeInput"]');
resolve({
lat: latInput?.value,
lng: lngInput?.value
});
}, 100);
});
}
return null;
});
// Verify second coordinates exist and are different from first
expect(secondCoords).toBeTruthy();
expect(secondCoords.lat).toBeTruthy();
expect(secondCoords.lng).toBeTruthy();
expect(firstCoords.lat).not.toBe(secondCoords.lat);
expect(firstCoords.lng).not.toBe(secondCoords.lng);
// Verify modal is still open
const modalOpen = await page.locator('[data-place-creation-target="modal"]').evaluate((modal) => {
return modal.classList.contains('modal-open');
});
expect(modalOpen).toBe(true);
});
});