Files
ui/src/api/endpoints.test.ts
T
Armen Rohalov 434854bf3c admin v2: implement design from ui_design/v2/plugin/admin.html
- Design system: v2 CSS variables (surface-0/1/2, border-hair, accent-amber/cyan/red/green/blue)
  and utility classes (.btn, .inp, .pill, .chip, .bracket, .panel, .seg, .swatch,
  .type-sq, .grid-bg, .ibtn, .checkbox, .tab); v1 az-* names aliased to v2 vars
  so other pages still render. Google Fonts (IBM Plex Sans + JetBrains Mono)
  loaded via <link> in index.html <head> to avoid FOUT.
- Header rebuilt to v2: amber wordmark + // divider, amber-bordered flight pill
  with cyan live dot, tab-style nav with amber underline on active, LINK status
  pill, cog + sign-out icon buttons.
- AdminPage rewritten to 3-column layout (340 / flex / 280):
  - Detection Classes: search + ADD button, table with #/Name/Hex/Ops columns,
    name-only inline edit with ringed swatch, sibling-row error alert.
  - AI Recognition Engine + GPS Device Link panels with corner-bracket borders,
    number steppers, segmented protocol control, dashed telemetry footers.
    Hooks (useAiSettings, useGpsSettings) seed factory defaults so the UI is
    interactive when GET fails (no backend).
  - Default Aircrafts: P/C/F type chips, isDefault star toggle, + ADD AIRCRAFT
    modal with model/type/resolution/maxMinutes/default fields.
- Co-located components: Modal (backdrop + ESC + body-scroll-lock),
  NumberStepper (▲▼ with clamp on click but not on typing), ClassEditRow.
- Types: Aircraft extended with FixedWing + optional resolution/maxMinutes;
  new AiRecognitionSettings/Telemetry, GpsDeviceSettings/Telemetry, GpsProtocol.
- Endpoints: /api/admin/ai-settings, /api/admin/gps-settings (+ /ping, /reconnect).
  POST /api/flights/aircrafts (plural REST collection).
- MSW: stateful admin-settings handler with resetAdminSettingsSeed() wired into
  tests/setup.ts. Aircraft seed expanded to 6 entries matching the mockup.
- i18n: full admin.{classes,aiEngine,gpsDevice,aircrafts} key sets in en+ua;
  nav.dataset shortened to "Dataset"; obsolete users-management keys removed.
- Tests: new AdminPage AI/GPS/aircraft test suites; admin_class_edit selectors
  updated for the name-only inline editor and the modal-based add flow.
2026-05-19 02:01:20 +03:00

272 lines
7.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { endpoints } from './endpoints'
import { endpoints as endpointsViaBarrel } from '../api'
// AZ-486 / F7 — this test file IS the wire-contract for the UI ↔ nginx layer
// (per `module-layout.md`'s "code-derived documentation" pattern referenced in
// the task spec). Every builder is asserted to produce the URL literal that
// existed in source before the refactor and that MSW handlers + e2e stubs
// intercept today. A change to any assertion below is a wire-contract change
// and MUST be coordinated with backend + MSW + e2e stubs in the same commit.
describe('AZ-486 endpoints — wire-contract URLs', () => {
describe('AC-1: admin', () => {
it('admin.authRefresh', () => {
// Assert
expect(endpoints.admin.authRefresh()).toBe('/api/admin/auth/refresh')
})
it('admin.authLogin', () => {
// Assert
expect(endpoints.admin.authLogin()).toBe('/api/admin/auth/login')
})
it('admin.authLogout', () => {
// Assert
expect(endpoints.admin.authLogout()).toBe('/api/admin/auth/logout')
})
it('admin.users', () => {
// Assert
expect(endpoints.admin.users()).toBe('/api/admin/users')
})
it('admin.usersMe (AZ-510 — bootstrap chain)', () => {
// Assert
expect(endpoints.admin.usersMe()).toBe('/api/admin/users/me')
})
it('admin.user(id) interpolates the id', () => {
// Assert
expect(endpoints.admin.user('abc')).toBe('/api/admin/users/abc')
})
it('admin.classes', () => {
// Assert
expect(endpoints.admin.classes()).toBe('/api/admin/classes')
})
it('admin.class(id) interpolates the id (string)', () => {
// Assert
expect(endpoints.admin.class('cls-7')).toBe('/api/admin/classes/cls-7')
})
it('admin.class(id) interpolates the id (number — DetectionClass.id today)', () => {
// Assert
expect(endpoints.admin.class(42)).toBe('/api/admin/classes/42')
})
it('admin.aiSettings', () => {
// Assert
expect(endpoints.admin.aiSettings()).toBe('/api/admin/ai-settings')
})
it('admin.gpsSettings', () => {
// Assert
expect(endpoints.admin.gpsSettings()).toBe('/api/admin/gps-settings')
})
it('admin.gpsPing', () => {
// Assert
expect(endpoints.admin.gpsPing()).toBe('/api/admin/gps-settings/ping')
})
it('admin.gpsReconnect', () => {
// Assert
expect(endpoints.admin.gpsReconnect()).toBe('/api/admin/gps-settings/reconnect')
})
})
describe('AC-1: annotations', () => {
it('annotations.classes', () => {
// Assert
expect(endpoints.annotations.classes()).toBe('/api/annotations/classes')
})
it('annotations.settingsUser', () => {
// Assert
expect(endpoints.annotations.settingsUser()).toBe(
'/api/annotations/settings/user',
)
})
it('annotations.settingsSystem', () => {
// Assert
expect(endpoints.annotations.settingsSystem()).toBe(
'/api/annotations/settings/system',
)
})
it('annotations.settingsDirectories', () => {
// Assert
expect(endpoints.annotations.settingsDirectories()).toBe(
'/api/annotations/settings/directories',
)
})
it('annotations.annotations', () => {
// Assert
expect(endpoints.annotations.annotations()).toBe(
'/api/annotations/annotations',
)
})
it('annotations.annotationsByMedia(mediaId) defaults pageSize=1000', () => {
// Assert
expect(endpoints.annotations.annotationsByMedia('m-1')).toBe(
'/api/annotations/annotations?mediaId=m-1&pageSize=1000',
)
})
it('annotations.annotationsByMedia(mediaId, pageSize) overrides pageSize', () => {
// Assert
expect(endpoints.annotations.annotationsByMedia('m-1', 50)).toBe(
'/api/annotations/annotations?mediaId=m-1&pageSize=50',
)
})
it('annotations.annotationImage(id)', () => {
// Assert
expect(endpoints.annotations.annotationImage('ann-7')).toBe(
'/api/annotations/annotations/ann-7/image',
)
})
it('annotations.annotationThumbnail(id)', () => {
// Assert
expect(endpoints.annotations.annotationThumbnail('ann-7')).toBe(
'/api/annotations/annotations/ann-7/thumbnail',
)
})
it('annotations.annotationEvents', () => {
// Assert
expect(endpoints.annotations.annotationEvents()).toBe(
'/api/annotations/annotations/events',
)
})
it('annotations.media(queryString)', () => {
// Assert
expect(endpoints.annotations.media('page=1&pageSize=50')).toBe(
'/api/annotations/media?page=1&pageSize=50',
)
})
it('annotations.mediaFile(id)', () => {
// Assert
expect(endpoints.annotations.mediaFile('m-1')).toBe(
'/api/annotations/media/m-1/file',
)
})
it('annotations.mediaItem(id)', () => {
// Assert
expect(endpoints.annotations.mediaItem('m-1')).toBe(
'/api/annotations/media/m-1',
)
})
it('annotations.mediaBatch', () => {
// Assert
expect(endpoints.annotations.mediaBatch()).toBe(
'/api/annotations/media/batch',
)
})
it('annotations.dataset(queryString)', () => {
// Assert
expect(endpoints.annotations.dataset('status=PENDING')).toBe(
'/api/annotations/dataset?status=PENDING',
)
})
it('annotations.datasetItem(annotationId)', () => {
// Assert
expect(endpoints.annotations.datasetItem('ann-7')).toBe(
'/api/annotations/dataset/ann-7',
)
})
it('annotations.datasetBulkStatus', () => {
// Assert
expect(endpoints.annotations.datasetBulkStatus()).toBe(
'/api/annotations/dataset/bulk-status',
)
})
it('annotations.datasetClassDistribution', () => {
// Assert
expect(endpoints.annotations.datasetClassDistribution()).toBe(
'/api/annotations/dataset/class-distribution',
)
})
})
describe('AC-1: flights', () => {
it('flights.collection() without query', () => {
// Assert
expect(endpoints.flights.collection()).toBe('/api/flights')
})
it('flights.collection(queryString) appends ?queryString', () => {
// Assert
expect(endpoints.flights.collection('pageSize=1000')).toBe(
'/api/flights?pageSize=1000',
)
})
it('flights.aircrafts', () => {
// Assert
expect(endpoints.flights.aircrafts()).toBe('/api/flights/aircrafts')
})
it('flights.aircraft(id)', () => {
// Assert
expect(endpoints.flights.aircraft('ac-1')).toBe(
'/api/flights/aircrafts/ac-1',
)
})
it('flights.flight(id)', () => {
// Assert
expect(endpoints.flights.flight('f-1')).toBe('/api/flights/f-1')
})
it('flights.flightWaypoints(id)', () => {
// Assert
expect(endpoints.flights.flightWaypoints('f-1')).toBe(
'/api/flights/f-1/waypoints',
)
})
it('flights.flightWaypoint(flightId, waypointId)', () => {
// Assert
expect(endpoints.flights.flightWaypoint('f-1', 'wp-2')).toBe(
'/api/flights/f-1/waypoints/wp-2',
)
})
it('flights.flightLiveGps(id)', () => {
// Assert
expect(endpoints.flights.flightLiveGps('f-1')).toBe(
'/api/flights/f-1/live-gps',
)
})
})
describe('AC-1: detect', () => {
it('detect.media(mediaId)', () => {
// Assert
expect(endpoints.detect.media('m-1')).toBe('/api/detect/m-1')
})
})
describe('AC-6: barrel re-export', () => {
it('endpoints is the same object when imported from src/api (the F4 barrel)', () => {
// Assert
expect(endpointsViaBarrel).toBe(endpoints)
})
})
})