Compare commits

...

4 Commits

Author SHA1 Message Date
mahula
9eb983cd03 fix typo 2025-10-05 12:13:06 +02:00
mahula
eeee4b9d01 add itemsearch tests to cypress search testing 2025-10-05 11:59:41 +02:00
mahula
81349e299b add custom cypress commands for search testing 2025-10-04 17:35:34 +02:00
mahula
37113c8ab4 add more seeding data to properly test the search functionality 2025-10-04 16:00:56 +02:00
7 changed files with 297 additions and 14 deletions

View File

@ -171,7 +171,7 @@ jobs:
working-directory: ./cypress
- name: Download test artifacts
uses: actions/upload-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5.0.0
uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5.0.0
with:
name: cypress-test-results-${{ github.run_id }}
path: ./cypress

View File

@ -51,6 +51,133 @@
]
},
"layer" : "layer-people"
},
{
"_sync_id": "item-places-2",
"name": "Community Garden Berlin",
"subname": "Organic vegetables and sustainable farming",
"text": "A thriving community garden in Berlin where neighbors grow organic vegetables together. We focus on #sustainability #community #gardening #organic practices. Join us for weekly workshops on #permaculture and #composting!",
"position": {
"type": "Point",
"coordinates": [
13.404954,
52.520008
]
},
"layer" : "layer-places"
},
{
"_sync_id": "item-event-2",
"name": "Tech Meetup Munich",
"text": "Monthly #technology #networking event for developers and tech enthusiasts. Topics include #javascript #opensource #webdev and #innovation. Great for #learning and meeting like-minded people!",
"position": {
"type": "Point",
"coordinates": [
11.576124,
48.137154
]
},
"layer" : "layer-events",
"start": "2025-12-15T18:00:00",
"end": "2025-12-15T21:00:00"
},
{
"_sync_id": "item-people-2",
"name": "Jane Developer",
"text": "Full-stack developer passionate about #opensource projects and #javascript frameworks. Interested in #sustainability tech and #community building. Always happy to collaborate on #innovation projects!",
"position": {
"type": "Point",
"coordinates": [
2.349014,
48.864716
]
},
"layer" : "layer-people"
},
{
"_sync_id": "item-places-3",
"name": "Café Collaboration London",
"subname": "Co-working space and coffee shop",
"text": "A vibrant co-working café in London perfect for #networking and #collaboration. Features excellent coffee, fast wifi, and a #community of freelancers and entrepreneurs. Regular #events and workshops on #business and #creativity.",
"position": {
"type": "Point",
"coordinates": [
-0.127758,
51.507351
]
},
"layer" : "layer-places"
},
{
"_sync_id": "item-event-3",
"name": "Sustainability Workshop NYC",
"text": "Learn about #sustainability practices and #environmental solutions in this hands-on workshop. Topics include #recycling #renewable energy and #green living. Perfect for #activists and #eco enthusiasts!",
"position": {
"type": "Point",
"coordinates": [
-74.005941,
40.712784
]
},
"layer" : "layer-events",
"start": "2025-11-20T14:00:00",
"end": "2025-11-20T17:00:00"
},
{
"_sync_id": "item-people-3",
"name": "Alex Entrepreneur",
"text": "Serial entrepreneur focused on #sustainability and #innovation. Building #community-driven solutions for #environmental challenges. Always looking for #collaboration opportunities in #greentech!",
"position": {
"type": "Point",
"coordinates": [
-122.419416,
37.774929
]
},
"layer" : "layer-people"
},
{
"_sync_id": "item-places-4",
"name": "Makerspace Tokyo",
"subname": "Innovation hub for creators",
"text": "State-of-the-art makerspace in Tokyo with 3D printers, laser cutters, and electronics lab. Perfect for #makers #innovation #prototyping and #learning. Join our #community of inventors and #entrepreneurs!",
"position": {
"type": "Point",
"coordinates": [
139.691706,
35.689487
]
},
"layer" : "layer-places"
},
{
"_sync_id": "item-event-4",
"name": "Open Source Conference",
"text": "Annual conference celebrating #opensource software and #collaboration. Features talks on #javascript #python #webdev and #community building. Great for #developers #learning and #networking!",
"position": {
"type": "Point",
"coordinates": [
-0.118092,
51.509865
]
},
"layer" : "layer-events",
"start": "2025-10-10T09:00:00",
"end": "2025-10-12T18:00:00"
},
{
"_sync_id": "item-places-5",
"name": "Test & Special Characters!",
"subname": "Edge case for search testing",
"text": "This item tests special characters: @#$%^&*()_+ and unicode: café naïve résumé. Also tests very long content that might affect search performance and display. Contains #testing #edgecase #unicode #special-chars tags.",
"position": {
"type": "Point",
"coordinates": [
4.902257,
52.367573
]
},
"layer" : "layer-places"
}
]
}

View File

@ -0,0 +1,50 @@
/// <reference types="cypress" />
describe('Utopia Map Item Search', () => {
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
cy.window().then((win) => {
win.sessionStorage.clear()
})
cy.visit('/')
cy.waitForMapReady()
})
it('should find items by exact name match', () => {
cy.searchFor('Tech Meetup Munich')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Tech Meetup Munich')
})
it('should find items by partial name match (case insensitive)', () => {
cy.searchFor('café collaboration')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Café Collaboration London')
})
it('should find items by text content', () => {
cy.searchFor('sustainability')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Alex Entrepreneur')
})
it('should navigate to item profile when clicking search result', () => {
cy.intercept('GET', '**/items*').as('getItems')
cy.visit('/')
cy.waitForMapReady()
cy.searchFor('makerspace')
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.get('[data-cy="search-item-result"]').contains('Makerspace Tokyo').click()
})
cy.url().should('match', /\/(item\/|[a-f0-9-]+)/)
cy.get('body').should('contain', 'Makerspace Tokyo')
// Verify search uses local filtering (items already loaded)
cy.get('@getItems.all').then((calls) => {
expect(calls.length).to.be.greaterThan(0)
})
})
})

View File

@ -87,6 +87,7 @@ export default tseslint.config(
'cypress/downloads/**',
'cypress/screenshots/**',
'cypress/videos/**',
'results/**',
'dist/**',
'build/**',
'*.min.js'

108
cypress/support/commands.ts Normal file
View File

@ -0,0 +1,108 @@
/// <reference types="cypress" />
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Clear the search input
* @example cy.clearSearch()
*/
clearSearch(): Chainable<Element>
/**
* Search for a term and wait for search suggestions to appear
* @param query - The search term to type
* @example cy.searchFor('berlin')
*/
searchFor(query: string): Chainable<Element>
/**
* Wait for the map and search components to be ready
* @example cy.waitForMapReady()
*/
waitForMapReady(): Chainable<Element>
/**
* Click on a map marker
* @example cy.clickMarker() // clicks first marker
*/
clickMarker(): Chainable<Element>
/**
* Wait for a popup to appear on the map
* @example cy.waitForPopup()
*/
waitForPopup(): Chainable<Element>
/**
* Close the currently open popup
* @example cy.closePopup()
*/
closePopup(): Chainable<Element>
/**
* Toggle a layer's visibility in the layer control
* @param layerName - Name of the layer to toggle
* @example cy.toggleLayer('places')
*/
toggleLayer(layerName: string): Chainable<Element>
/**
* Open the layer control panel
* @example cy.openLayerControl()
*/
openLayerControl(): Chainable<Element>
/**
* Close the layer control panel
* @example cy.closeLayerControl()
*/
closeLayerControl(): Chainable<Element>
}
}
}
Cypress.Commands.add('clearSearch', () => {
cy.get('[data-cy="search-input"]').clear()
})
Cypress.Commands.add('searchFor', (query: string) => {
cy.get('[data-cy="search-input"]').clear()
cy.get('[data-cy="search-input"]').type(query)
cy.get('[data-cy="search-suggestions"]', { timeout: 10000 }).should('be.visible')
})
Cypress.Commands.add('waitForMapReady', () => {
cy.get('[data-cy="search-input"]', { timeout: 10000 }).should('be.visible')
cy.get('.leaflet-container', { timeout: 10000 }).should('be.visible')
cy.get('.leaflet-marker-icon', { timeout: 15000 }).should('have.length.at.least', 1)
})
Cypress.Commands.add('clickMarker', () => {
// For now, always use force click since markers might be clustered or outside viewport
cy.get('.leaflet-marker-icon').first().click({ force: true })
})
Cypress.Commands.add('waitForPopup', () => {
cy.get('[data-cy="item-popup"]', { timeout: 10000 }).should('be.visible')
})
Cypress.Commands.add('closePopup', () => {
cy.get('.leaflet-popup-close-button').click()
})
Cypress.Commands.add('toggleLayer', (layerName: string) => {
cy.get(`[data-cy="layer-checkbox-${layerName}"]`).click()
})
Cypress.Commands.add('openLayerControl', () => {
cy.get('[data-cy="layer-control-button"]').click()
cy.get('[data-cy="layer-control-panel"]', { timeout: 5000 }).should('be.visible')
})
Cypress.Commands.add('closeLayerControl', () => {
cy.get('[data-cy="layer-control-close"]').click()
})
export {}

View File

@ -1,19 +1,10 @@
/// <reference types="cypress" />
// Import commands.ts using ES2015 syntax:
// import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
import './commands'
// This file is processed and loaded automatically before your test files.
// This is a great place to put global configuration and behavior that modifies Cypress.
// You can change the location of this file or turn off
// automatically serving support files with the 'supportFile' configuration option.
// Global exception handler
Cypress.on('uncaught:exception', (err) => {
console.log('Uncaught exception:', err.message)

View File

@ -113,6 +113,7 @@ export const SearchControl = () => {
autoComplete='off'
value={value}
className='tw:input tw:input-bordered tw:h-12 tw:grow tw:shadow-xl tw:rounded-box tw:pr-12 tw:w-full'
data-cy='search-input'
ref={searchInput}
onChange={(e) => setValue(e.target.value)}
onFocus={() => {
@ -124,6 +125,7 @@ export const SearchControl = () => {
{value.length > 0 && (
<button
className='tw:btn tw:btn-sm tw:btn-circle tw:absolute tw:right-2 tw:top-2'
data-cy='search-clear-button'
onClick={() => setValue('')}
>
@ -140,13 +142,14 @@ export const SearchControl = () => {
value.length === 0 ? (
''
) : (
<div className='tw:card tw:card-body tw:bg-base-100 tw:p-4 tw:mt-2 tw:shadow-xl tw:overflow-y-auto tw:max-h-[calc(100dvh-152px)] tw:absolute tw:z-3000 tw:w-83'>
<div className='tw:card tw:card-body tw:bg-base-100 tw:p-4 tw:mt-2 tw:shadow-xl tw:overflow-y-auto tw:max-h-[calc(100dvh-152px)] tw:absolute tw:z-3000 tw:w-83' data-cy='search-suggestions'>
{tagsResults.length > 0 && (
<div className='tw:flex tw:flex-wrap'>
{tagsResults.slice(0, 3).map((tag) => (
<div
key={tag.name}
className='tw:rounded-2xl tw:text-white tw:p-1 tw:px-4 tw:shadow-md tw:card tw:mr-2 tw:mb-2 tw:cursor-pointer'
data-cy='search-tag-result'
style={{ backgroundColor: tag.color }}
onClick={() => {
addFilterTag(tag)
@ -165,6 +168,7 @@ export const SearchControl = () => {
<div
key={item.id}
className='tw:cursor-pointer tw:hover:font-bold tw:flex tw:flex-row'
data-cy='search-item-result'
onClick={() => {
const marker = Object.entries(leafletRefs).find((r) => r[1].item === item)?.[1]
.marker
@ -178,7 +182,7 @@ export const SearchControl = () => {
}}
>
{item.layer?.markerIcon.image ? (
<div className='tw:w-7 tw:h-full tw:flex tw:justify-center tw:items-center'>
<div className='tw:w-7 tw:h-full tw:flex tw:justify-center tw:items-center' data-cy='search-item-icon'>
<SVG
src={appState.assetsApi.url + item.layer.markerIcon.image}
className='tw:text-current tw:mr-2 tw:mt-0'
@ -191,7 +195,7 @@ export const SearchControl = () => {
/>
</div>
) : (
<div className='tw:w-7' />
<div className='tw:w-7' data-cy='search-item-icon-placeholder' />
)}
<div>
<div className='tw:text-sm tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
@ -207,6 +211,7 @@ export const SearchControl = () => {
{Array.from(geoResults).map((geo) => (
<div
className='tw:flex tw:flex-row tw:hover:font-bold tw:cursor-pointer'
data-cy='search-geo-result'
key={Math.random()}
onClick={() => {
searchInput.current?.blur()
@ -262,6 +267,7 @@ export const SearchControl = () => {
{isGeoCoordinate(value) && (
<div
className='tw:flex tw:flex-row tw:hover:font-bold tw:cursor-pointer'
data-cy='search-coordinate-result'
onClick={() => {
marker(
new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]),