mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:21:11 +00:00
434854bf3c
- 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.
102 lines
3.6 KiB
TypeScript
102 lines
3.6 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
import { http } from 'msw'
|
|
import { server } from '../../../../tests/msw/server'
|
|
import { jsonResponse, errorResponse } from '../../../../tests/msw/helpers'
|
|
import { renderWithProviders, screen, waitFor, userEvent } from '../../../../tests/helpers/render'
|
|
import { seedBearer, clearBearer } from '../../../../tests/helpers/auth'
|
|
import { AdminPage } from '..'
|
|
|
|
// v2 admin — AI Recognition Engine panel. Covers GET → render telemetry,
|
|
// edit value via stepper / input, APPLY → PATCH, RESET → discards draft,
|
|
// PATCH 500 → inline error.
|
|
//
|
|
// Both AI and GPS panels render APPLY buttons; AI is the first one in DOM
|
|
// order. We pick [0] from getAllByRole rather than coupling to internal markup.
|
|
|
|
function aiApplyButton(): HTMLElement {
|
|
return screen.getAllByRole('button', { name: /apply/i })[0]
|
|
}
|
|
function aiResetButton(): HTMLElement {
|
|
return screen.getByRole('button', { name: /reset/i })
|
|
}
|
|
|
|
beforeEach(() => {
|
|
seedBearer()
|
|
})
|
|
afterEach(() => {
|
|
clearBearer()
|
|
})
|
|
|
|
describe('AdminPage — AI Recognition Engine', () => {
|
|
it('renders initial settings + telemetry from GET /api/admin/ai-settings', async () => {
|
|
renderWithProviders(<AdminPage />)
|
|
expect(await screen.findByText('YOLOV8-X · CKPT-241')).toBeInTheDocument()
|
|
expect(screen.getByDisplayValue('4')).toBeInTheDocument()
|
|
expect(screen.getByDisplayValue('25')).toBeInTheDocument()
|
|
})
|
|
|
|
it('APPLY sends PATCH with edited settings and reflects telemetry refresh', async () => {
|
|
const calls: { body: unknown }[] = []
|
|
server.use(
|
|
http.patch('/api/admin/ai-settings', async ({ request }) => {
|
|
const body = await request.json()
|
|
calls.push({ body })
|
|
return jsonResponse({
|
|
settings: { framesToRecognize: 8, minSecondsBetween: 2, minConfidence: 25 },
|
|
telemetry: {
|
|
model: 'YOLOV8-X', checkpoint: 'CKPT-242',
|
|
lastRunAt: '2026-05-18T12:00:00Z', frames: 99, avgConfidence: 80,
|
|
},
|
|
})
|
|
}),
|
|
)
|
|
renderWithProviders(<AdminPage />)
|
|
await screen.findByText('YOLOV8-X · CKPT-241')
|
|
|
|
const framesInput = screen.getByDisplayValue('4') as HTMLInputElement
|
|
await userEvent.clear(framesInput)
|
|
await userEvent.type(framesInput, '8')
|
|
|
|
await userEvent.click(aiApplyButton())
|
|
|
|
await waitFor(() => expect(calls.length).toBe(1))
|
|
expect((calls[0].body as { framesToRecognize: number }).framesToRecognize).toBe(8)
|
|
expect(await screen.findByText(/CKPT-242/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('RESET reverts draft to the last persisted value (no PATCH)', async () => {
|
|
const patchCalls: unknown[] = []
|
|
server.use(
|
|
http.patch('/api/admin/ai-settings', () => {
|
|
patchCalls.push({})
|
|
return jsonResponse({})
|
|
}),
|
|
)
|
|
renderWithProviders(<AdminPage />)
|
|
await screen.findByText('YOLOV8-X · CKPT-241')
|
|
|
|
const framesInput = screen.getByDisplayValue('4') as HTMLInputElement
|
|
await userEvent.clear(framesInput)
|
|
await userEvent.type(framesInput, '9')
|
|
expect(screen.getByDisplayValue('9')).toBeInTheDocument()
|
|
|
|
await userEvent.click(aiResetButton())
|
|
|
|
expect(screen.getByDisplayValue('4')).toBeInTheDocument()
|
|
expect(patchCalls.length).toBe(0)
|
|
})
|
|
|
|
it('PATCH 500 surfaces an inline error', async () => {
|
|
server.use(
|
|
http.patch('/api/admin/ai-settings', () => errorResponse(500, 'boom')),
|
|
)
|
|
renderWithProviders(<AdminPage />)
|
|
await screen.findByText('YOLOV8-X · CKPT-241')
|
|
|
|
await userEvent.click(aiApplyButton())
|
|
|
|
const alert = await screen.findByRole('alert')
|
|
expect(alert.textContent ?? '').toMatch(/failed to save ai/i)
|
|
})
|
|
})
|