mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-04-06 01:25:33 +00:00
Compare commits
4 Commits
770b6de5c0
...
9eb983cd03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb983cd03 | ||
|
|
eeee4b9d01 | ||
|
|
81349e299b | ||
|
|
37113c8ab4 |
2
.github/workflows/test.e2e.yml
vendored
2
.github/workflows/test.e2e.yml
vendored
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
50
cypress/e2e/search/item-search.cy.ts
Normal file
50
cypress/e2e/search/item-search.cy.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -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
108
cypress/support/commands.ts
Normal 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 {}
|
||||
@ -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)
|
||||
|
||||
@ -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]),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user