Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions frontend/src/tests/Banner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Banner from '../components/Banner'

describe('Banner', () => {
it('shows the UFOund it site title', () => {
render(<Banner />)

expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
'UFOund it',
)
})
})
48 changes: 48 additions & 0 deletions frontend/src/tests/SearchBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import SearchBar from '../components/SearchBar'

describe('SearchBar', () => {
it('renders the current query and sort option', () => {
render(
<SearchBar
query="Oslo"
onQueryChange={vi.fn()}
sort="city-asc"
onSortChange={vi.fn()}
/>,
)

expect(screen.getByRole('searchbox', { name: 'Search sightings' })).toHaveValue(
'Oslo',
)
expect(screen.getByRole('combobox', { name: 'Sort by' })).toHaveValue(
'city-asc',
)
})

it('reports query and sort changes without submitting the form', () => {
const onQueryChange = vi.fn()
const onSortChange = vi.fn()
const { container } = render(
<SearchBar
query=""
onQueryChange={onQueryChange}
sort="date-desc"
onSortChange={onSortChange}
/>,
)

fireEvent.change(screen.getByRole('searchbox'), {
target: { value: 'triangle' },
})
fireEvent.change(screen.getByRole('combobox'), {
target: { value: 'shape-asc' },
})
fireEvent.submit(container.querySelector('form')!)

expect(onQueryChange).toHaveBeenCalledWith('triangle')
expect(onSortChange).toHaveBeenCalledWith('shape-asc')
expect(container.querySelector('form')).toBeInTheDocument()
})
})
93 changes: 93 additions & 0 deletions frontend/src/tests/SearchPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { afterEach, describe, expect, it, vi } from 'vitest'
import SearchPage from '../pages/SearchPage'

vi.mock('../components/SightingMap', () => ({
default: () => <div data-testid="sighting-map" />,
}))

function renderSearchPage(initialEntry = '/') {
return render(
<MemoryRouter initialEntries={[initialEntry]}>
<SearchPage />
</MemoryRouter>,
)
}

describe('SearchPage', () => {
afterEach(() => {
vi.useRealTimers()
})

it('loads a query from the URL and filters after the debounce delay', () => {
vi.useFakeTimers()
renderSearchPage('/?q=Oslo')

expect(screen.getByRole('searchbox')).toHaveValue('Oslo')
expect(screen.getByText('1 result')).toBeInTheDocument()

act(() => vi.advanceTimersByTime(300))

expect(screen.getByText('1 result')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Oslo, NO' })).toBeInTheDocument()
})

it('updates displayed results when the search and sort controls change', () => {
vi.useFakeTimers()
renderSearchPage()

expect(screen.getByRole('combobox', { name: 'Sort by' })).toHaveValue('date-desc')
expect(screen.getAllByRole('link')[0]).toHaveTextContent('Bergen, NO')

fireEvent.change(screen.getByRole('searchbox'), {
target: { value: 'Phoenix' },
})
act(() => vi.advanceTimersByTime(300))

expect(screen.getByRole('link', { name: 'Phoenix, AZ, US' })).toBeInTheDocument()
expect(screen.getByText('1 result')).toBeInTheDocument()

fireEvent.change(screen.getByRole('searchbox'), { target: { value: '' } })
act(() => vi.advanceTimersByTime(300))
fireEvent.change(screen.getByRole('combobox', { name: 'Sort by' }), {
target: { value: 'city-asc' },
})

const locations = screen
.getAllByRole('link')
.map((link) => link.textContent)
expect(locations[0]).toBe('Bergen, NO')
expect(locations[1]).toBe('Edna, TX, US')
})

it('sorts results oldest first', () => {
vi.useFakeTimers()
renderSearchPage()

fireEvent.change(screen.getByRole('combobox', { name: 'Sort by' }), {
target: { value: 'date-asc' },
})

const locations = screen
.getAllByRole('link')
.map((link) => link.textContent)
expect(locations[0]).toBe('Phoenix, AZ, US')
expect(locations[1]).toBe('San Marcos, TX, US')
})

it('sorts results newest first', () => {
vi.useFakeTimers()
renderSearchPage()

fireEvent.change(screen.getByRole('combobox', { name: 'Sort by' }), {
target: { value: 'date-desc' },
})

const locations = screen
.getAllByRole('link')
.map((link) => link.textContent)
expect(locations[0]).toBe('Bergen, NO')
expect(locations[1]).toBe('Tromsø, NO')
})
})
57 changes: 57 additions & 0 deletions frontend/src/tests/SightingDetailPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router'
import { afterEach, describe, expect, it, vi } from 'vitest'
import SightingDetailPage from '../pages/SightingDetailPage'

vi.mock('../components/SightingMap', () => ({
default: ({
interactive,
zoom,
}: {
interactive?: boolean
zoom?: number
}) => <div data-testid="detail-map" data-interactive={interactive} data-zoom={zoom} />,
}))

function renderDetailPage(path: string, state?: unknown) {
return render(
<MemoryRouter initialEntries={[{ pathname: path, state }]}>
<Routes>
<Route path="/sightings/:id" element={<SightingDetailPage />} />
</Routes>
</MemoryRouter>,
)
}

describe('SightingDetailPage', () => {
afterEach(() => {
vi.restoreAllMocks()
})

it('renders sighting details and preserves the originating search link', () => {
vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
renderDetailPage('/sightings/1', { fromSearch: '?q=San+Marcos' })

expect(screen.getByRole('heading', { name: 'San Marcos, TX, US' })).toBeInTheDocument()
expect(screen.getByText('45 min')).toBeInTheDocument()
expect(screen.getByText(/This event took place/)).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Back to results' })).toHaveAttribute(
'href',
'/?q=San+Marcos',
)
expect(screen.getByTestId('detail-map')).toHaveAttribute('data-interactive', 'true')
expect(screen.getByTestId('detail-map')).toHaveAttribute('data-zoom', '11')
expect(document.title).toBe('San Marcos, TX, US · UFOund it')
})

it('handles missing sightings and links back to the unfiltered list', () => {
vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
renderDetailPage('/sightings/missing')

expect(screen.getByRole('heading', { name: 'Sighting not found' })).toBeInTheDocument()
expect(screen.getByText('There is no sighting with id “missing”.')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Back to results' })).toHaveAttribute('href', '/')
expect(screen.queryByTestId('detail-map')).not.toBeInTheDocument()
expect(document.title).toBe('Sighting not found · UFOund it')
})
})
42 changes: 42 additions & 0 deletions frontend/src/tests/SightingList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router'
import { describe, expect, it, vi } from 'vitest'
import SightingList from '../components/SightingList'
import { mockSightings } from '../data/mock-sightings'

vi.mock('../components/SightingMap', () => ({
default: () => <div data-testid="sighting-map" />,
}))

describe('SightingList', () => {
it('renders sightings, formatted fields, and detail links', () => {
render(
<MemoryRouter initialEntries={['/?q=Texas']}>
<SightingList sightings={[mockSightings[0]]} />
</MemoryRouter>,
)

expect(screen.getByText('1 result')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'San Marcos, TX, US' })).toHaveAttribute(
'href',
'/sightings/1',
)
expect(screen.getByText('cylinder')).toBeInTheDocument()
expect(screen.getByText('45 min')).toBeInTheDocument()
expect(screen.getByText(mockSightings[0].comments)).toBeInTheDocument()
expect(screen.getByTestId('sighting-map')).toBeInTheDocument()
})

it('uses plural result text and explains when no sightings match', () => {
render(
<MemoryRouter>
<SightingList sightings={[]} />
</MemoryRouter>,
)

expect(screen.getByText('0 results')).toBeInTheDocument()
expect(
screen.getByText(/No sightings match your search/),
).toBeInTheDocument()
})
})
75 changes: 75 additions & 0 deletions frontend/src/tests/SightingMap.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { ReactNode } from 'react'
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import SightingMap from '../components/SightingMap'
import { mockSightings } from '../data/mock-sightings'

type MapProps = {
children: ReactNode
center: [number, number]
zoom: number
dragging: boolean
zoomControl: boolean
doubleClickZoom: boolean
touchZoom: boolean
boxZoom: boolean
keyboard: boolean
}

vi.mock('react-leaflet', () => ({
MapContainer: ({ children, center, zoom, dragging, zoomControl, keyboard }: MapProps) => (
<div
data-testid="map-container"
data-center={center.join(',')}
data-zoom={zoom}
data-dragging={dragging}
data-zoom-control={zoomControl}
data-keyboard={keyboard}
>
{children}
</div>
),
TileLayer: ({ url }: { url: string }) => <div data-testid="tile-layer" data-url={url} />,
Marker: ({ position, children }: { position: [number, number]; children: ReactNode }) => (
<div data-testid="map-marker" data-position={position.join(',')}>
{children}
</div>
),
Popup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))

describe('SightingMap', () => {
it('shows the sighting location on a non-interactive thumbnail by default', () => {
const sighting = mockSightings[0]
render(<SightingMap sighting={sighting} />)

expect(screen.getByLabelText('Map showing San Marcos')).toBeInTheDocument()
expect(screen.getByTestId('map-container')).toHaveAttribute(
'data-center',
`${sighting.latitude},${sighting.longitude}`,
)
expect(screen.getByTestId('map-container')).toHaveAttribute('data-zoom', '10')
expect(screen.getByTestId('map-container')).toHaveAttribute('data-dragging', 'false')
expect(screen.getByTestId('map-container')).toHaveAttribute(
'data-zoom-control',
'false',
)
expect(screen.getByTestId('map-marker')).toHaveAttribute(
'data-position',
`${sighting.latitude},${sighting.longitude}`,
)
expect(screen.getByText('San Marcos, TX, US')).toBeInTheDocument()
})

it('enables map controls when interactive', () => {
render(<SightingMap sighting={mockSightings[0]} interactive zoom={11} />)

expect(screen.getByTestId('map-container')).toHaveAttribute('data-zoom', '11')
expect(screen.getByTestId('map-container')).toHaveAttribute('data-dragging', 'true')
expect(screen.getByTestId('map-container')).toHaveAttribute(
'data-zoom-control',
'true',
)
expect(screen.getByTestId('map-container')).toHaveAttribute('data-keyboard', 'true')
})
})
Loading