Files
TREK/client/src/components/Map/placePhoto.test.ts
T
Maurice 98e2938033 fix(maps): let an uploaded place image fill its marker
A place with an uploaded image showed a plain disc of its category
colour on the map, while a picked suggestion looked fine. The marker
sized its img with width and height attributes, and stylesheet rules
beat those: Tailwind's preflight gives every img height: auto, and
Leaflet's marker pane adds width: auto. Suggested and auto photos reach
the marker as 48px square thumbs, so nothing showed. A phone photo kept
its own proportions on the GL map, a sliver inside the circle, and its
full pixel size on Leaflet, where only the rounded-off corner of a
4000px image lands inside the circle. Both maps now build the img
through one helper that sizes it in its style, so object-fit: cover
crops any photo to the circle.

The thumb loading on both maps was keyed on place ids, so a change of
picture under the same place went unnoticed until a reload:
- Taking an upload off a place left the category dot instead of
  bringing its auto photo back.
- Picking another suggested photo kept the thumb of the first one,
  which was cached under the provider id. A picked photo is now its own
  cache key.
2026-09-19 13:35:44 +02:00

68 lines
3.3 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { isCustomPlaceImage, markerPhotoHtml, photoCacheKey, photoSourcesKey } from './placePhoto'
function imgOf(html: string): HTMLImageElement {
const holder = document.createElement('div')
holder.innerHTML = html
return holder.querySelector('img')!
}
describe('isCustomPlaceImage', () => {
it('FE-MAP-PLACEPHOTO-001: an upload or an inline image is shown as it is, a provider photo is not', () => {
expect(isCustomPlaceImage('/uploads/places/a.jpg')).toBe(true)
expect(isCustomPlaceImage('data:image/png;base64,AAA')).toBe(true)
expect(isCustomPlaceImage('/api/maps/place-photo/gp-1~p0/bytes')).toBe(false)
expect(isCustomPlaceImage(null)).toBe(false)
})
})
describe('markerPhotoHtml', () => {
it('FE-MAP-PLACEPHOTO-002: sizes the picture by its style, where no stylesheet rule can undo it', () => {
// Preflight's `height: auto` and Leaflet's `width: auto` both outrank a width or
// height attribute, so a photo that is not a small square has to be pinned here.
const img = imgOf(markerPhotoHtml('/uploads/places/wide.jpg'))
expect(img.getAttribute('src')).toBe('/uploads/places/wide.jpg')
expect(img.style.width).toBe('100%')
expect(img.style.height).toBe('100%')
expect(img.hasAttribute('width')).toBe(false)
expect(img.hasAttribute('height')).toBe(false)
})
it('FE-MAP-PLACEPHOTO-003: escapes the url, which a user can set', () => {
const html = markerPhotoHtml('/uploads/x" onerror="alert(1)" y="')
expect(html).not.toContain('onerror="alert(1)"')
expect(imgOf(html).getAttribute('src')).toBe('/uploads/x" onerror="alert(1)" y="')
})
})
describe('photoCacheKey', () => {
it('FE-MAP-PLACEPHOTO-004: a picked provider photo is its own key, ahead of the provider id', () => {
expect(photoCacheKey({ image_url: '/api/maps/place-photo/gp-1~p2/bytes', google_place_id: 'gp-1', lat: 1, lng: 2 }))
.toBe('/api/maps/place-photo/gp-1~p2/bytes')
})
it('FE-MAP-PLACEPHOTO-005: otherwise the provider id, then the coordinates, then nothing', () => {
expect(photoCacheKey({ image_url: '/uploads/places/a.jpg', google_place_id: 'gp-1', osm_id: 'node/1' })).toBe('gp-1')
expect(photoCacheKey({ osm_id: 'node/1', lat: 1, lng: 2 })).toBe('node/1')
expect(photoCacheKey({ lat: 0, lng: 0 })).toBe('0,0')
expect(photoCacheKey({ image_url: 'https://example.com/a.jpg', lat: null, lng: null })).toBe('')
})
})
describe('photoSourcesKey', () => {
const place = { id: 7, google_place_id: 'gp-7', lat: 48, lng: 2 }
it('FE-MAP-PLACEPHOTO-006: changes when an upload comes off or another photo is picked', () => {
const withUpload = photoSourcesKey([{ ...place, image_url: '/uploads/places/a.jpg' }])
const bare = photoSourcesKey([{ ...place, image_url: null }])
const picked = photoSourcesKey([{ ...place, image_url: '/api/maps/place-photo/gp-7~p0/bytes' }])
const pickedAgain = photoSourcesKey([{ ...place, image_url: '/api/maps/place-photo/gp-7~p1/bytes' }])
expect(new Set([withUpload, bare, picked, pickedAgain]).size).toBe(4)
})
it('FE-MAP-PLACEPHOTO-007: stays put when one upload replaces another, which needs no thumb', () => {
expect(photoSourcesKey([{ ...place, image_url: '/uploads/places/a.jpg' }]))
.toBe(photoSourcesKey([{ ...place, image_url: '/uploads/places/b.jpg' }]))
})
})