Files
TREK/client/src/components/Planner/placeNavigation.test.ts
T

198 lines
8.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { getNavigationTargets, navigationAppChoices, navigationTargetLabel, openNavigationTarget, preferredNavAppOptions, showsAppleMaps, showsGeoUri, withPreferredApp } from './placeNavigation'
import { useSettingsStore } from '../../store/settingsStore'
import type { Place } from '../../types'
// FE-PLANNER-NAV-001 to FE-PLANNER-NAV-008
function place(overrides: Partial<Place> = {}): Place {
return {
name: 'Stephansdom',
lat: 48.2038,
lng: 16.3616,
google_place_id: null,
google_ftid: null,
...overrides,
} as unknown as Place
}
/**
* jsdom reports its own agent, so Apple has to be simulated. userAgent lives on
* the prototype as a getter: shadowing it on the instance and deleting that
* again afterwards restores the real one without having to reconstruct it.
*/
function withUserAgent(ua: string) {
Object.defineProperty(navigator, 'userAgent', { value: ua, configurable: true })
return () => { delete (navigator as unknown as Record<string, unknown>).userAgent }
}
afterEach(() => { vi.restoreAllMocks() })
describe('getNavigationTargets', () => {
it('FE-PLANNER-NAV-001: offers every app that can resolve a place with coordinates', () => {
const targets = getNavigationTargets(place())
expect(targets.map(t => t.id)).toEqual(['google', 'waze', 'apple', 'osm', 'comaps'])
expect(targets[0].label).toBe('Google Maps')
})
it('FE-PLANNER-NAV-002: Waze gets the name as well as the position, and navigates', () => {
// Coordinates alone would show a driver a number instead of a destination;
// the name alone would be a guess. Waze documents taking both.
const waze = getNavigationTargets(place()).find(t => t.id === 'waze')!
expect(waze.url).toBe('https://waze.com/ul?q=Stephansdom&ll=48.2038,16.3616&navigate=yes')
})
it('FE-PLANNER-NAV-003: Google keeps its precise link when the place has an ftid', () => {
const targets = getNavigationTargets(place({ google_ftid: '0x882b:0x8591' }))
// The whole point of the ftid: it lands on the entry, not on the roof.
expect(targets[0].url).toContain('ftid=0x882b:0x8591')
expect(targets[0].url).not.toContain('query=48.2038')
})
it('FE-PLANNER-NAV-004: a Windows desktop gets Apple Maps too, through its web version', () => {
const restore = withUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)')
try {
expect(getNavigationTargets(place()).map(t => t.id)).toContain('apple')
} finally { restore() }
})
it('FE-PLANNER-NAV-004b: an Android phone does not, because nobody there wants it', () => {
const restore = withUserAgent('Mozilla/5.0 (Linux; Android 15; Pixel 9)')
try {
expect(getNavigationTargets(place()).map(t => t.id)).toEqual(['google', 'waze', 'osm', 'comaps', 'geo'])
} finally { restore() }
})
it('FE-PLANNER-NAV-005: an iPhone gets Apple Maps with a navigation target', () => {
const restore = withUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)')
try {
const targets = getNavigationTargets(place())
expect(targets.map(t => t.id)).toEqual(['google', 'waze', 'apple', 'osm', 'comaps'])
// q next to ll labels the pin rather than searching blindly.
expect(targets[2].url).toBe('https://maps.apple.com/?q=Stephansdom&ll=48.2038,16.3616')
} finally { restore() }
})
it('FE-PLANNER-NAV-006: iPadOS reports itself as a Mac, and both deserve the entry', () => {
const restore = withUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)')
try {
expect(showsAppleMaps()).toBe(true)
} finally { restore() }
})
it('FE-PLANNER-NAV-007: a place without coordinates keeps only what Google can resolve', () => {
// Waze and Apple Maps take coordinates and nothing else, so they cannot
// offer anything here — Google still can, through the place id.
// OpenStreetMap still manages a name search, the two driving apps do not.
const targets = getNavigationTargets(place({ lat: null, lng: null, google_place_id: 'ChIJabc' }))
expect(targets.map(t => t.id)).toEqual(['google', 'osm'])
})
it('FE-PLANNER-NAV-007b: CoMaps needs the position, so a place without one loses it', () => {
// CoMaps drops a pin from `ll` and only labels it with `n` — a name alone
// has nothing to attach to, the same reason Waze and Apple Maps drop out.
const targets = getNavigationTargets(place({ lat: null, lng: null, name: 'Stephansdom' }))
expect(targets.map(t => t.id)).not.toContain('comaps')
})
it('FE-PLANNER-NAV-104: Android gets a geo: link for any installed map app, labeled in the UI language (#1406)', () => {
const restore = withUserAgent('Mozilla/5.0 (Linux; Android 15; Pixel 9)')
try {
expect(showsGeoUri()).toBe(true)
const geo = getNavigationTargets(place()).find(t => t.id === 'geo')!
expect(geo.url).toBe('geo:48.2038,16.3616?q=48.2038,16.3616(Stephansdom)')
expect(navigationTargetLabel(geo, key => `t:${key}`)).toBe('t:inspector.otherMapApp')
} finally { restore() }
})
it('FE-PLANNER-NAV-105: no geo: entry where nothing would answer it', () => {
const restore = withUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)')
try {
expect(getNavigationTargets(place()).map(t => t.id)).not.toContain('geo')
const google = getNavigationTargets(place())[0]
expect(navigationTargetLabel(google, key => `t:${key}`)).toBe('Google Maps')
} finally { restore() }
})
it('FE-PLANNER-NAV-008: no place at all yields nothing', () => {
expect(getNavigationTargets(null)).toEqual([])
expect(getNavigationTargets(place({ lat: null, lng: null, name: '' }))).toEqual([])
})
it('FE-PLANNER-NAV-009: a nameless place still reaches every app, just without a label', () => {
const targets = getNavigationTargets(place({ name: '' }))
expect(targets.map(t => t.id)).toEqual(['google', 'waze', 'apple', 'osm', 'comaps'])
expect(targets.find(t => t.id === 'waze')!.url).toBe('https://waze.com/ul?ll=48.2038,16.3616&navigate=yes')
})
})
describe('openNavigationTarget', () => {
const target = { id: 'google', label: 'Google Maps', url: 'https://maps.google.com/?q=1,2' } as const
afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks() })
function stubDisplayMode(installed: boolean, iosStandalone = false) {
const open = vi.fn()
const assigned: string[] = []
vi.stubGlobal('window', {
matchMedia: (q: string) => ({ matches: installed && q.includes('display-mode') }),
navigator: { standalone: iosStandalone },
open,
get location() { return { get href() { return '' }, set href(v: string) { assigned.push(v) } } },
})
return { open, assigned }
}
it('FE-PLANNER-NAV-101: a browser tab keeps opening a tab', () => {
const { open, assigned } = stubDisplayMode(false)
openNavigationTarget(target)
expect(open).toHaveBeenCalledWith(target.url, '_blank', 'noopener,noreferrer')
expect(assigned).toEqual([])
})
it('FE-PLANNER-NAV-102: an installed app hands over from the current context, leaving no blank window (#2218)', () => {
const { open, assigned } = stubDisplayMode(true)
openNavigationTarget(target)
expect(open).not.toHaveBeenCalled()
expect(assigned).toEqual([target.url])
})
it('FE-PLANNER-NAV-103: an iOS home-screen app is recognised without the display-mode query', () => {
const { open, assigned } = stubDisplayMode(false, true)
openNavigationTarget(target)
expect(open).not.toHaveBeenCalled()
expect(assigned).toEqual([target.url])
})
})
describe('preferred map app (#2423)', () => {
afterEach(() => {
useSettingsStore.setState(s => ({ settings: { ...s.settings, preferred_nav_app: '' } }))
})
it('FE-PLANNER-NAV-2423-1: unset keeps every app on offer', () => {
expect(getNavigationTargets(place()).length).toBeGreaterThan(1)
})
it('FE-PLANNER-NAV-2423-2: a picked app is the only target, so the button opens it straight away', () => {
useSettingsStore.setState(s => ({ settings: { ...s.settings, preferred_nav_app: 'waze' } }))
const targets = getNavigationTargets(place())
expect(targets.map(t => t.id)).toEqual(['waze'])
})
it('FE-PLANNER-NAV-2423-3: an app this place cannot be opened in falls back to the full list', () => {
const targets = [{ id: 'google' as const, label: 'Google Maps', url: 'g' }, { id: 'osm' as const, label: 'OpenStreetMap', url: 'o' }]
expect(withPreferredApp(targets, 'amap')).toEqual(targets)
expect(withPreferredApp(targets, null)).toEqual(targets)
expect(withPreferredApp(targets, 'osm')).toEqual([targets[1]])
})
it('FE-PLANNER-NAV-2423-4: the settings options start with asking and name every app', () => {
const options = preferredNavAppOptions(k => k)
expect(options[0]).toEqual({ value: '', label: 'settings.preferredNavAppAsk' })
expect(options.slice(1).map(o => o.value)).toEqual(navigationAppChoices().map(a => a.id))
expect(options.map(o => o.value)).toContain('google')
})
})