Files
ui/src/features/flights/__tests__/satellite_tile.test.tsx
T
Armen Rohalov ff522b0821
ci/woodpecker/push/build-arm Pipeline failed
flights v2: implement redesign
Migrate src/features/flights to the v2 tactical-ops design — the last
page still on the legacy az-* palette — keeping all existing planner
behavior (Leaflet map, draw modes, import/export, altitude dialog).

- Restyle every flights surface to v2 tokens and shared classes:
  flight roster sidebar (search, rows, telemetry card), params panel,
  waypoint list, altitude/JSON dialogs, map-point popup, altitude
  chart, wind inputs, mini-map.
- Rebuild the params panel to the mockup order (draw-mode selector,
  Mission Config, Waypoints) with existing controls appended.
- Add HUD overlays on the real Leaflet map (telemetry, legend, compass,
  zoom/recenter toolbar, bottom status strip); disable the default zoom
  control, add a dark tactical-grid backdrop, and use the legend glyphs
  (diamond/square/octagon) plus a pulsing amber current-position beacon.
- Add a functional GPS-Denied panel: orthophoto upload (local),
  live-GPS readout fed by the existing SSE stream, and a GPS-correction
  form that patches waypoint coordinates.
- Extract a shared drawModes config used by the panel and collapse rail.
- Add flights.v2 i18n keys to en.json and ua.json (parity preserved).
2026-06-03 01:23:10 +03:00

218 lines
7.2 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import type L from 'leaflet'
import { renderWithProviders, screen } from '../../../../tests/helpers/render'
// AZ-498 — self-hosted satellite tiles + drop classic/satellite toggle.
//
// Colocated under src/features/flights/__tests__/ per module-layout's "Tests"
// guidance: keeps the cross-component import surface clean (these tests
// reach into 05_flights internals — `./FlightMap`, `./MiniMap`, `./types` —
// which is intra-component access). Tests/ is reserved for cross-cutting
// black-box suites whose imports must go through public-API barrels.
//
// Covers the spec's fast-profile ACs:
// AC-1 — env-resolved getTileUrl() returns the env var verbatim.
// AC-2 — when the env var is unset, getTileUrl() returns the dev default
// `http://localhost:5100/tiles/{z}/{x}/{y}` (cycle-2 assumption).
// AC-3 — every <TileLayer> the SPA renders sets crossOrigin="use-credentials"
// so the browser attaches the satellite-provider auth cookie.
// AC-4 — the classic/satellite toggle, the `mapType` state, and the
// `MiniMap.Props.mapType` prop are all gone.
//
// Notes:
// - AC-5 is statically enforced by tsc on the new ImportMetaEnv shape +
// the `.env.example` audit; no runtime test needed.
// - AC-6, AC-7 are e2e/contract; AC-8 in the original spec misattributed
// `tile_split_zoom*` (image-annotation surface) — see implementation
// report. AC-9 is enforced by STC-ARCH-01 / STC-ARCH-02.
interface TileLayerProps {
url?: string
crossOrigin?: string
attribution?: string
}
interface MapContainerProps {
children?: React.ReactNode
className?: string
}
vi.mock('react-leaflet', () => ({
MapContainer: ({ children, className }: MapContainerProps) => (
<div data-testid="map-container" className={className}>{children}</div>
),
TileLayer: (props: TileLayerProps) => (
<img
data-testid="tile-layer"
data-tile-url={props.url ?? ''}
data-cross-origin={props.crossOrigin ?? ''}
data-attribution={props.attribution ?? ''}
alt=""
/>
),
Marker: () => null,
Popup: () => null,
Polyline: () => null,
Rectangle: () => null,
CircleMarker: () => null,
useMap: () => ({
on: () => undefined,
off: () => undefined,
setView: () => undefined,
removeLayer: () => undefined,
getCenter: () => ({ lat: 0, lng: 0 }),
invalidateSize: () => undefined,
}),
useMapEvents: () => null,
}))
// Leaflet itself is touched at import time by FlightMap (`L.polyline`,
// `L.Symbol.arrowHead`). Mock the bits the component reaches for so the
// import doesn't blow up under jsdom.
vi.mock('leaflet', () => {
const Lstub = {
polyline: () => ({ addTo: () => Lstub.polyline(), on: () => undefined }),
polylineDecorator: () => ({ addTo: () => undefined }),
Symbol: { arrowHead: () => ({}) },
Icon: { Default: class { mergeOptions() {} } },
Marker: class {},
Layer: class {},
LatLngBounds: class {},
}
return { default: Lstub }
})
vi.mock('leaflet/dist/leaflet.css', () => ({}))
vi.mock('leaflet-polylinedecorator', () => ({}))
vi.mock('../DrawControl', () => ({ default: () => null }))
vi.mock('../MapPoint', () => ({ default: () => null }))
vi.mock('../mapIcons', () => ({ currentPositionIcon: {} }))
import FlightMap from '../FlightMap'
import MiniMap from '../MiniMap'
import { getTileUrl, DEFAULT_SATELLITE_TILE_URL } from '../types'
const stubLatLng = { lat: 0, lng: 0 } as unknown as L.LatLng
const fixedPosition = { lat: 50, lng: 30 }
const baseFlightMapProps = {
points: [],
calculatedPointInfo: [],
currentPosition: fixedPosition,
rectangles: [],
setRectangles: () => undefined,
rectangleColor: 'red',
actionMode: 'points' as const,
onAddPoint: () => undefined,
onUpdatePoint: () => undefined,
onRemovePoint: () => undefined,
onAltitudeChange: () => undefined,
onMetaChange: () => undefined,
onPolylineClick: () => undefined,
onPositionChange: () => undefined,
onMapMove: () => undefined,
}
describe('AZ-498 — getTileUrl() env resolution', () => {
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-1: returns the env-set VITE_SATELLITE_TILE_URL verbatim', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', 'http://satellite-provider:5100/tiles/{z}/{x}/{y}')
// Assert
expect(getTileUrl()).toBe('http://satellite-provider:5100/tiles/{z}/{x}/{y}')
})
it('AC-2: returns the dev default when VITE_SATELLITE_TILE_URL is unset', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
// Assert
expect(getTileUrl()).toBe(DEFAULT_SATELLITE_TILE_URL)
expect(DEFAULT_SATELLITE_TILE_URL).toBe('http://localhost:5100/tiles/{z}/{x}/{y}')
})
it('AC-2: strips trailing slashes off the env-set URL', () => {
// Arrange
vi.stubEnv('VITE_SATELLITE_TILE_URL', 'http://satellite-provider:5100/tiles/{z}/{x}/{y}/')
// Assert
expect(getTileUrl()).toBe('http://satellite-provider:5100/tiles/{z}/{x}/{y}')
})
})
describe('AZ-498 — FlightMap satellite-only TileLayer', () => {
beforeEach(() => {
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-3: <TileLayer> declares crossOrigin="use-credentials"', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-cross-origin')).toBe('use-credentials')
})
it('AC-3: <TileLayer> renders the dev-default URL when env is unset', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-tile-url')).toBe(DEFAULT_SATELLITE_TILE_URL)
})
it('AC-4: the classic/satellite toggle button is gone', () => {
// Act
renderWithProviders(<FlightMap {...baseFlightMapProps} />, { withoutAuth: true })
// Assert
expect(screen.queryByRole('button', { name: /satellite|classic/i })).toBeNull()
// Only one <TileLayer> is mounted (no per-mode branching).
expect(screen.getAllByTestId('tile-layer')).toHaveLength(1)
})
})
describe('AZ-498 — MiniMap satellite-only TileLayer', () => {
beforeEach(() => {
vi.stubEnv('VITE_SATELLITE_TILE_URL', '')
})
afterEach(() => {
vi.unstubAllEnvs()
})
it('AC-3: MiniMap <TileLayer> declares crossOrigin="use-credentials"', () => {
// Act
renderWithProviders(
<MiniMap pointPosition={{ x: 0, y: 0, latlng: stubLatLng }} />,
{ withoutAuth: true },
)
// Assert
const tile = screen.getByTestId('tile-layer')
expect(tile.getAttribute('data-cross-origin')).toBe('use-credentials')
})
it('AC-4: MiniMap mounts with only `pointPosition` prop (no `mapType`)', () => {
// Act — explicitly omit mapType; if MiniMap still required it, TS would
// error at compile time. The runtime render also confirms the component
// mounts with just the position prop.
renderWithProviders(
<MiniMap pointPosition={{ x: 0, y: 0, latlng: stubLatLng }} />,
{ withoutAuth: true },
)
// Assert
expect(screen.getByTestId('tile-layer')).toBeInTheDocument()
})
})