dawarich/e2e/v2/map/search.spec.js
Evgenii Burmakin 8934c29fce
0.36.2 (#2007)
* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>
2025-12-06 20:54:49 +01:00

333 lines
12 KiB
JavaScript

import { test, expect } from '@playwright/test'
import { closeOnboardingModal } from '../../helpers/navigation.js'
import { waitForMapLibre, waitForLoadingComplete } from '../helpers/setup.js'
/**
* Helper to open settings panel and switch to Search tab
* @param {Page} page - Playwright page object
*/
async function openSearchTab(page) {
await page.click('button[title="Open map settings"]')
await page.waitForTimeout(400)
await page.click('button[title="Search"]')
await page.waitForTimeout(200)
}
test.describe('Location Search', () => {
// Increase timeout for search tests as they involve network requests
test.setTimeout(60000)
test.beforeEach(async ({ page }) => {
await page.goto('/map/v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59')
await closeOnboardingModal(page)
await waitForMapLibre(page)
await waitForLoadingComplete(page)
await page.waitForTimeout(1500)
})
test.describe('Search UI', () => {
test('displays search input in settings panel', async ({ page }) => {
// Open settings panel
await openSearchTab(page)
// Search tab should be active by default
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
await expect(searchInput).toBeVisible()
await expect(searchInput).toHaveAttribute('placeholder', 'Enter name of a place')
})
test('search results container exists', async ({ page }) => {
await openSearchTab(page)
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
await expect(resultsContainer).toBeAttached()
await expect(resultsContainer).toHaveClass(/hidden/)
})
})
test.describe('Search Functionality', () => {
test('typing in search input triggers search', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Type a search query (3+ chars to trigger search)
await searchInput.fill('New')
// Wait for results container to become visible or stay hidden (with timeout)
// Search might show results or "no results" - both are valid
try {
await resultsContainer.waitFor({ state: 'visible', timeout: 3000 })
// Results appeared
expect(await resultsContainer.isVisible()).toBe(true)
} catch (e) {
// Results might still be hidden if search returned nothing
// This is acceptable behavior
console.log('Search did not return visible results')
}
})
test('short queries do not trigger search', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Type single character (should not trigger search - minimum is 3 chars)
await searchInput.fill('N')
// Wait a bit for any potential search to trigger
await page.waitForTimeout(500)
// Results should stay hidden (search not triggered for short query)
await expect(resultsContainer).toHaveClass(/hidden/)
})
test('clearing search clears results', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Type search query
await searchInput.fill('Berlin')
// Wait for potential search results
await page.waitForTimeout(1000)
// Clear input
await searchInput.clear()
await page.waitForTimeout(300)
// Results should be hidden after clearing
await expect(resultsContainer).toHaveClass(/hidden/)
})
})
test.describe('Search Integration', () => {
test('search manager is initialized', async ({ page }) => {
// Wait for controller to be fully initialized
await page.waitForTimeout(1000)
const hasSearchManager = await page.evaluate(() => {
const element = document.querySelector('[data-controller*="maps--maplibre"]')
if (!element) return false
const app = window.Stimulus || window.Application
if (!app) return false
const controller = app.getControllerForElementAndIdentifier(element, 'maps--maplibre')
return controller?.searchManager !== undefined
})
// Search manager should exist if search targets are present
const hasSearchTargets = await page.locator('[data-maps--maplibre-target="searchInput"]').count()
if (hasSearchTargets > 0) {
expect(hasSearchManager).toBe(true)
}
})
test('search input has autocomplete disabled', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
await expect(searchInput).toHaveAttribute('autocomplete', 'off')
})
})
test.describe('Visit Search and Creation', () => {
test('clicking on suggestion shows visits', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Search for a location
await searchInput.fill('Sterndamm')
await page.waitForTimeout(800) // Wait for debounce + API
// Wait for suggestions to appear
const firstSuggestion = resultsContainer.locator('.search-result-item').first()
await expect(firstSuggestion).toBeVisible({ timeout: 5000 })
// Click on first suggestion
await firstSuggestion.click()
await page.waitForTimeout(1500) // Wait for visits API call
// Results container should show visits or "no visits found"
const hasVisits = await resultsContainer.locator('.location-result').count()
const hasNoVisitsMessage = await resultsContainer.locator('text=No visits found').count()
expect(hasVisits > 0 || hasNoVisitsMessage > 0).toBe(true)
})
test('visits are grouped by year with expand/collapse', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Search and select location
await searchInput.fill('Sterndamm')
await page.waitForTimeout(800)
const firstSuggestion = resultsContainer.locator('.search-result-item').first()
await expect(firstSuggestion).toBeVisible({ timeout: 5000 })
await firstSuggestion.click()
await page.waitForTimeout(1500)
// Check if year toggles exist
const yearToggle = resultsContainer.locator('.year-toggle').first()
const hasYearToggle = await yearToggle.count()
if (hasYearToggle > 0) {
// Year visits should be hidden initially
const yearVisits = resultsContainer.locator('.year-visits').first()
await expect(yearVisits).toHaveClass(/hidden/)
// Click year toggle to expand
await yearToggle.click()
await page.waitForTimeout(300)
// Year visits should now be visible
await expect(yearVisits).not.toHaveClass(/hidden/)
}
})
test('clicking on visit item opens create visit modal', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Search and select location
await searchInput.fill('Sterndamm')
await page.waitForTimeout(800)
const firstSuggestion = resultsContainer.locator('.search-result-item').first()
await expect(firstSuggestion).toBeVisible({ timeout: 5000 })
await firstSuggestion.click()
await page.waitForTimeout(1500)
// Check if there are visits
const yearToggle = resultsContainer.locator('.year-toggle').first()
const hasVisits = await yearToggle.count()
if (hasVisits > 0) {
// Expand year section
await yearToggle.click()
await page.waitForTimeout(300)
// Click on first visit item
const visitItem = resultsContainer.locator('.visit-item').first()
await visitItem.click()
await page.waitForTimeout(500)
// Modal should appear
const modal = page.locator('#create-visit-modal')
await expect(modal).toBeVisible()
// Modal should have form fields
await expect(modal.locator('input[name="name"]')).toBeVisible()
await expect(modal.locator('input[name="started_at"]')).toBeVisible()
await expect(modal.locator('input[name="ended_at"]')).toBeVisible()
// Close modal
await modal.locator('button:has-text("Cancel")').click()
await page.waitForTimeout(500)
}
})
test('create visit modal has prefilled data', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Search and select location
await searchInput.fill('Sterndamm')
await page.waitForTimeout(800)
const firstSuggestion = resultsContainer.locator('.search-result-item').first()
await expect(firstSuggestion).toBeVisible({ timeout: 5000 })
await firstSuggestion.click()
await page.waitForTimeout(1500)
// Check if there are visits
const yearToggle = resultsContainer.locator('.year-toggle').first()
const hasVisits = await yearToggle.count()
if (hasVisits > 0) {
// Expand and click visit
await yearToggle.click()
await page.waitForTimeout(300)
const visitItem = resultsContainer.locator('.visit-item').first()
await visitItem.click()
await page.waitForTimeout(500)
const modal = page.locator('#create-visit-modal')
await expect(modal).toBeVisible()
// Name should be prefilled
const nameInput = modal.locator('input[name="name"]')
const nameValue = await nameInput.inputValue()
expect(nameValue.length).toBeGreaterThan(0)
// Start and end times should be prefilled
const startInput = modal.locator('input[name="started_at"]')
const startValue = await startInput.inputValue()
expect(startValue.length).toBeGreaterThan(0)
const endInput = modal.locator('input[name="ended_at"]')
const endValue = await endInput.inputValue()
expect(endValue.length).toBeGreaterThan(0)
// Close modal
await modal.locator('button:has-text("Cancel")').click()
await page.waitForTimeout(500)
}
})
test('results container height allows viewing multiple visits', async ({ page }) => {
await openSearchTab(page)
const resultsContainer = page.locator('[data-maps--maplibre-target="searchResults"]')
// Check max-height class is set appropriately (max-h-96)
const hasMaxHeight = await resultsContainer.evaluate(el => {
const classes = el.className
return classes.includes('max-h-96') || classes.includes('max-h')
})
expect(hasMaxHeight).toBe(true)
})
})
test.describe('Accessibility', () => {
test('search input is keyboard accessible', async ({ page }) => {
await openSearchTab(page)
const searchInput = page.locator('[data-maps--maplibre-target="searchInput"]')
// Focus input with keyboard
await searchInput.focus()
await expect(searchInput).toBeFocused()
// Type with keyboard
await page.keyboard.type('Paris')
await page.waitForTimeout(500)
const value = await searchInput.inputValue()
expect(value).toBe('Paris')
})
test('search has descriptive label', async ({ page }) => {
await openSearchTab(page)
const label = page.locator('label:has-text("Search for a place")')
await expect(label).toBeVisible()
})
})
})