mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Card "Onde estou" no dashboard staff: point-in-polygon contra a malha municipal que o painel do mapa ja carrega, sem artefato de centroides, sem migration, sem collection, sem server action e sem Consent -- a posicao e lida por navigator.geolocation e casada no browser, nunca sai do aparelho. - src/lib/municipalityProximity.ts: haversine, ray casting, centroide ponderado por area e uniao discriminada (inScope | zoneCity | outOfScope | outsideBahia) - src/utilities/campaignGeolocation.ts: flag de sessao, leitura guardada de permissao e requestCurrentPosition com falha tipada - Salvador nao ganha ZE chutada: link para a lista filtrada, com href serializado no servidor (importar o serializador canonico no cliente custava 21 kB de First Load JS) - fallback fora da carteira ordenado por centroide, teto de 150 km - fix grosseiro (>10 km) mostra o resultado com ressalva de precisao /campanha First Load JS: 265 -> 268 kB.
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
GEO_PROMPT_SESSION_KEY,
|
|
hasPromptedThisSession,
|
|
markPromptedThisSession,
|
|
readGeolocationPermissionState,
|
|
requestCurrentPosition,
|
|
} from '@/utilities/campaignGeolocation'
|
|
import { stub } from '../helpers/stub'
|
|
|
|
const defineNavigatorProperty = (property: 'geolocation' | 'permissions', value: unknown) => {
|
|
Object.defineProperty(navigator, property, { value, configurable: true, writable: true })
|
|
}
|
|
|
|
const removeNavigatorProperty = (property: 'geolocation' | 'permissions') => {
|
|
Reflect.deleteProperty(navigator, property)
|
|
}
|
|
|
|
const positionErrorOf = (code: number): GeolocationPositionError =>
|
|
stub<GeolocationPositionError>({
|
|
code,
|
|
message: 'stub',
|
|
PERMISSION_DENIED: 1,
|
|
POSITION_UNAVAILABLE: 2,
|
|
TIMEOUT: 3,
|
|
})
|
|
|
|
const stubPosition = (coords: { latitude: number; longitude: number; accuracy: number }) => {
|
|
const getCurrentPosition = vi.fn(
|
|
(
|
|
onSuccess: PositionCallback,
|
|
_onError?: PositionErrorCallback | null,
|
|
_options?: PositionOptions,
|
|
) =>
|
|
void onSuccess(
|
|
stub<GeolocationPosition>({ coords: stub<GeolocationCoordinates>(coords), timestamp: 0 }),
|
|
),
|
|
)
|
|
defineNavigatorProperty('geolocation', { getCurrentPosition })
|
|
return getCurrentPosition
|
|
}
|
|
|
|
const stubPositionError = (code: number) => {
|
|
defineNavigatorProperty('geolocation', {
|
|
getCurrentPosition: (_onSuccess: PositionCallback, onError: PositionErrorCallback) =>
|
|
void onError(positionErrorOf(code)),
|
|
})
|
|
}
|
|
|
|
describe('campaignGeolocation', () => {
|
|
afterEach(() => {
|
|
sessionStorage.clear()
|
|
removeNavigatorProperty('geolocation')
|
|
removeNavigatorProperty('permissions')
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('session prompt flag', () => {
|
|
it('starts unset and survives within the session', () => {
|
|
expect(hasPromptedThisSession()).toBe(false)
|
|
|
|
markPromptedThisSession()
|
|
|
|
expect(sessionStorage.getItem(GEO_PROMPT_SESSION_KEY)).toBe('1')
|
|
expect(hasPromptedThisSession()).toBe(true)
|
|
})
|
|
|
|
it('stays silent when storage throws (private mode)', () => {
|
|
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
throw new Error('denied')
|
|
})
|
|
|
|
expect(hasPromptedThisSession()).toBe(true)
|
|
})
|
|
|
|
it('swallows write failures', () => {
|
|
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('quota')
|
|
})
|
|
|
|
expect(() => markPromptedThisSession()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('readGeolocationPermissionState', () => {
|
|
it('is unknown without the geolocation API', async () => {
|
|
defineNavigatorProperty('permissions', { query: vi.fn() })
|
|
|
|
await expect(readGeolocationPermissionState()).resolves.toBe('unknown')
|
|
})
|
|
|
|
it('is unknown when the Permissions API is absent (Safari)', async () => {
|
|
stubPosition({ latitude: -12.9, longitude: -38.5, accuracy: 30 })
|
|
|
|
await expect(readGeolocationPermissionState()).resolves.toBe('unknown')
|
|
})
|
|
|
|
it('reports the queried state', async () => {
|
|
stubPosition({ latitude: -12.9, longitude: -38.5, accuracy: 30 })
|
|
defineNavigatorProperty('permissions', {
|
|
query: vi.fn().mockResolvedValue({ state: 'granted' }),
|
|
})
|
|
|
|
await expect(readGeolocationPermissionState()).resolves.toBe('granted')
|
|
})
|
|
|
|
it('is unknown when the query rejects (unsupported descriptor)', async () => {
|
|
stubPosition({ latitude: -12.9, longitude: -38.5, accuracy: 30 })
|
|
defineNavigatorProperty('permissions', {
|
|
query: vi.fn().mockRejectedValue(new TypeError('unsupported')),
|
|
})
|
|
|
|
await expect(readGeolocationPermissionState()).resolves.toBe('unknown')
|
|
})
|
|
})
|
|
|
|
describe('requestCurrentPosition', () => {
|
|
it('returns a fix with low accuracy requested', async () => {
|
|
const getCurrentPosition = stubPosition({
|
|
latitude: -12.973,
|
|
longitude: -38.5121,
|
|
accuracy: 42,
|
|
})
|
|
|
|
await expect(requestCurrentPosition()).resolves.toEqual({
|
|
ok: true,
|
|
fix: { lat: -12.973, lng: -38.5121, accuracyM: 42 },
|
|
})
|
|
expect(getCurrentPosition.mock.calls[0]?.[2]).toMatchObject({ enableHighAccuracy: false })
|
|
})
|
|
|
|
it('reports an unsupported browser instead of throwing', async () => {
|
|
await expect(requestCurrentPosition()).resolves.toEqual({ ok: false, reason: 'unsupported' })
|
|
})
|
|
|
|
it.each([
|
|
[1, 'denied'],
|
|
[2, 'unavailable'],
|
|
[3, 'timeout'],
|
|
])('maps error code %i to %s', async (code, reason) => {
|
|
stubPositionError(code)
|
|
|
|
await expect(requestCurrentPosition()).resolves.toEqual({ ok: false, reason })
|
|
})
|
|
})
|
|
})
|