Files
ui/src/components/Header.test.tsx
T
Oleksandr Bezdieniezhnykh 70fb452805 [AZ-510] Auth bootstrap: POST refresh + chained /users/me
Replace the broken `GET /api/admin/auth/refresh` (no `credentials:'include'`)
mount-time bootstrap with `POST /api/admin/auth/refresh` (with credentials)
chained to `GET /api/admin/users/me`. Returning users with a valid HttpOnly
refresh cookie no longer flash through `/login`. Closes Finding B3 / Vision P3.

- Add module-scoped `bootstrapInflight` guard (StrictMode double-mount safety)
  + test-only reset hook exported via the `src/auth` barrel; `tests/setup.ts`
  resets it in `afterEach` to prevent pending-promise leakage between tests.
- Defensive `hasPermission` against legacy `/users/me` payloads omitting
  `permissions`; default MSW handler now seeds `permissions` explicitly.
- Add `endpoints.admin.usersMe()` builder (STC-ARCH-02 forbids the literal).
- Bulk-swap 15 test files from `http.get` -> `http.post` for the refresh
  override so intentional bootstrap-fail tests still fail correctly.
- Update auth component description; mark B3 closed.
- Code review verdict PASS; static + fast suites green (231 / 13 skipped).

Batch report: _docs/03_implementation/batch_13_cycle3_report.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 02:59:31 +03:00

209 lines
8.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from 'msw'
import { Route, Routes } from 'react-router-dom'
import userEvent from '@testing-library/user-event'
import type { ReactNode } from 'react'
import { server } from '../../tests/msw/server'
import { jsonResponse, paginate } from '../../tests/msw/helpers'
import { renderWithProviders, screen, waitFor } from '../../tests/helpers/render'
import { seedBearer, clearBearer } from '../../tests/helpers/auth'
import { seedFlights } from '../../tests/fixtures/seed_flights'
import { opAlice, seedPermissions } from '../../tests/fixtures/seed_users'
import { FlightProvider } from './FlightContext'
import Header from './Header'
// AZ-468 — Header flight-dropdown a11y + Escape handler.
// FT-P-30 closed-state a11y — aria-expanded=false, accessible trigger name
// FT-P-31 open-state a11y — aria-expanded=true, role=listbox/menu,
// aria-activedescendant points to a real id
// FT-N-09 Escape close + detach — Escape closes the dropdown and the
// document-level Escape handler is removed
// (no leakage into other components).
//
// Production status (today): src/components/Header.tsx renders a plain
// <button> trigger and a <div> menu without any aria-* attributes, and the
// dropdown has NO Escape key handler (only a `mousedown` listener for
// click-outside). All three task ACs therefore fail today; FT-P-30/31 are
// captured as documented DRIFT via `it.fails()` (flips green when production
// gains the attributes); FT-N-09 is QUARANTINEd via `it.skip` because the
// behavior is wholly absent — there is no addEventListener('keydown', ...) in
// the dropdown to assert against.
function HeaderHarness({ children }: { children?: ReactNode }) {
return <FlightProvider>{children}<Header /></FlightProvider>
}
function mountHeader() {
return renderWithProviders(
<Routes>
<Route
path="/flights"
element={<HeaderHarness><div data-testid="content" /></HeaderHarness>}
/>
<Route path="/login" element={<div data-testid="login-route" />} />
</Routes>,
{ initialEntries: ['/flights'] },
)
}
function wireAuthAndFlights() {
server.use(
// AZ-510 — bootstrap = POST refresh -> { token } + chained GET /users/me.
http.post('/api/admin/auth/refresh', () => jsonResponse({ token: 'test-bearer-default' })),
http.get('/api/admin/users/me', () =>
jsonResponse({ ...opAlice, permissions: seedPermissions[opAlice.id] ?? [] }),
),
http.get('/api/flights', ({ request }) => {
const url = new URL(request.url)
const pageSize = Number(url.searchParams.get('pageSize') ?? '50')
return jsonResponse(paginate(seedFlights, 1, pageSize))
}),
http.get('/api/annotations/settings/user', () =>
jsonResponse({
id: 'us-1', userId: opAlice.id, selectedFlightId: null,
annotationsLeftPanelWidth: null, annotationsRightPanelWidth: null,
datasetLeftPanelWidth: null, datasetRightPanelWidth: null,
}),
),
http.put('/api/annotations/settings/user', async ({ request }) => {
const body = await request.json()
return jsonResponse(body)
}),
)
}
describe('AZ-468 / src/components/Header.tsx — flight dropdown', () => {
beforeEach(() => {
seedBearer()
wireAuthAndFlights()
})
afterEach(() => {
clearBearer()
vi.restoreAllMocks()
})
describe('FT-P-30 — closed-state a11y', () => {
it.fails(
'trigger advertises aria-expanded=false when the menu is closed (drift: attribute currently missing)',
async () => {
mountHeader()
// Wait for the flights list to have been fetched so the trigger is hydrated.
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
// AC-1 contract: aria-expanded=false when closed; no aria-activedescendant.
expect(trigger).toHaveAttribute('aria-expanded', 'false')
expect(trigger).not.toHaveAttribute('aria-activedescendant')
},
)
it('control — closed trigger today lacks aria-expanded entirely (drift seen)', async () => {
mountHeader()
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
expect(trigger).not.toHaveAttribute('aria-expanded')
})
})
describe('FT-P-31 — open-state a11y', () => {
it.fails(
'opened dropdown advertises aria-expanded=true and listbox/menu role with a real aria-activedescendant (drift: attributes missing today)',
async () => {
const user = userEvent.setup()
mountHeader()
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
await user.click(trigger)
// AC-2 contract.
expect(trigger).toHaveAttribute('aria-expanded', 'true')
const listbox = screen.getByRole('listbox')
expect(listbox).toBeInTheDocument()
const optionId = trigger.getAttribute('aria-activedescendant')
expect(optionId).toBeTruthy()
expect(document.getElementById(optionId as string)).not.toBeNull()
},
)
it('control — opened dropdown today exposes options but with no role and no aria wiring (drift seen)', async () => {
const user = userEvent.setup()
mountHeader()
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
await user.click(trigger)
// The filter input renders on open, so the panel is visibly open.
expect(screen.getByPlaceholderText(/filter/i)).toBeInTheDocument()
// But none of the listbox roles or aria-activedescendant wiring exists yet.
expect(screen.queryByRole('listbox')).toBeNull()
expect(trigger).not.toHaveAttribute('aria-activedescendant')
})
})
describe('FT-N-09 — Escape close + document-level handler detached', () => {
it.skip(
'QUARANTINE (no production behavior): Escape closes the dropdown and the document keydown handler is removed',
async () => {
// When the production code lands a document-level keydown listener that
// handles Escape, this test asserts:
// 1. Pressing Escape closes the dropdown (filter input gone)
// 2. The document.addEventListener('keydown', ...) call made when the
// dropdown opened is paired with a removeEventListener('keydown', ...)
// with the SAME handler reference when the dropdown closes (verified
// via spies on document.addEventListener/removeEventListener).
// The test below is a sketch of the assertion shape — left skipped because
// Header has no keydown listener today and asserting against absent code
// would produce noise, not signal.
const addSpy = vi.spyOn(document, 'addEventListener')
const removeSpy = vi.spyOn(document, 'removeEventListener')
const user = userEvent.setup()
mountHeader()
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
await user.click(trigger)
const keydownAdds = addSpy.mock.calls.filter(([type]) => type === 'keydown')
expect(keydownAdds.length).toBeGreaterThanOrEqual(1)
await user.keyboard('{Escape}')
expect(screen.queryByPlaceholderText(/filter/i)).toBeNull()
const keydownRemoves = removeSpy.mock.calls.filter(([type, fn]) =>
type === 'keydown' && fn === keydownAdds[0]?.[1],
)
expect(keydownRemoves.length).toBeGreaterThanOrEqual(1)
},
)
it('control — Escape today is a no-op; the dropdown stays open (drift seen)', async () => {
const user = userEvent.setup()
mountHeader()
await waitFor(() =>
expect(screen.getByRole('button', { name: /select flight/i })).toBeInTheDocument(),
)
const trigger = screen.getByRole('button', { name: /select flight/i })
await user.click(trigger)
expect(screen.getByPlaceholderText(/filter/i)).toBeInTheDocument()
await user.keyboard('{Escape}')
// QUARANTINE evidence: the filter input is still present — Escape did
// nothing because the Header has no keydown handler today.
expect(screen.getByPlaceholderText(/filter/i)).toBeInTheDocument()
})
})
})