mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 20:11:13 +00:00
[AZ-458] [AZ-467] [AZ-468] [AZ-482] Batch 3 - SSE/RBAC/Header/security tests
Implements 4 blackbox-test tasks for AZ-455 Phase A baseline:
- AZ-458 SSE lifecycle + bearer rotation: 9 fast tests (8 pass, 1
QUARANTINE for annotation-status); 4 e2e scenarios (gated by suite
stack). Uses tests/helpers/sse-mock.ts with globalThis.EventSource
monkey-patch per AC-3 (no stub of src/api/sse.ts). AC-2 bearer
rotation captured as documented drift via it.fails() — FlightsPage
useEffect deps do not include the token today.
- AZ-467 ProtectedRoute spinner + timeout + RBAC: 9 new fast tests
extending the AZ-457 file (6 pass, 3 QUARANTINE), plus 3 e2e
scenarios. FT-P-32 spinner a11y is it.fails() drift; FT-P-33 timeout
and FT-N-03/05 RBAC redirects are it.skip QUARANTINE (no production
behavior today). Positive control: admin_carol reaches /admin.
- AZ-468 Header flight-dropdown a11y: 6 fast tests (5 pass, 1
QUARANTINE). FT-P-30/31 are it.fails() drift (aria-expanded /
role=listbox / aria-activedescendant currently missing); FT-N-09
is it.skip QUARANTINE (no document keydown handler exists).
- AZ-482 Secrets + banned-libs + AC-N1 anti-criterion: 3 new static
checks (STC-SEC13 legacy integrations, STC-SEC14 concurrent-edit,
STC-SEC1B dist/ OWM key) plus refactor of 4 existing checks
(STC-N2/N4/S13/S6) to read from tests/security/banned-deps.json
via scripts/check-banned-deps.mjs per AZ-482 constraint
("deny-list lives in tests/security/banned-deps.json so additions
are visible in code review"). All 22 static checks PASS.
Totals: 57 fast tests pass + 9 skipped; 22/22 static checks pass.
Self-review verdict PASS_WITH_WARNINGS — all five findings are
documented drifts captured by it.fails() / it.skip QUARANTINE +
control tests. See _docs/03_implementation/batch_03_report.md
for the per-task / per-AC matrix and recommended Phase B follow-up
production tasks (Header a11y; ProtectedRoute spinner/timeout/RBAC;
SSE bearer-rotation reconnect; AnnotationsPage SSE).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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(
|
||||
http.get('/api/admin/auth/refresh', () =>
|
||||
jsonResponse({ token: 'test-bearer-default', user: { ...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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user