mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 15:21:11 +00:00
70fb452805
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>
158 lines
6.4 KiB
TypeScript
158 lines
6.4 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { http } from 'msw'
|
|
import { server } from './msw/server'
|
|
import { jsonResponse, paginate } from './msw/helpers'
|
|
import { renderWithProviders, screen, waitFor } from './helpers/render'
|
|
import { seedBearer, clearBearer } from './helpers/auth'
|
|
import { FlightProvider, Header } from '../src/components'
|
|
|
|
// AZ-469 — Browser support + responsive variants.
|
|
//
|
|
// AC-1 (FT-P-34): Chromium + Firefox smoke on /flights, /annotations, /dataset.
|
|
// Pure e2e (Playwright two-project config) — covered in
|
|
// e2e/tests/browser_support_responsive.e2e.ts.
|
|
// AC-2 (FT-P-35): At viewport 480 px the bottom-nav is rendered; the desktop
|
|
// top-bar is hidden. Tailwind drives this via `sm:hidden` /
|
|
// `hidden sm:flex`. JSDOM does not compute media queries, so
|
|
// the fast test asserts the structural marker — i.e., the
|
|
// bottom-nav element exists with the `sm:hidden` class chain
|
|
// and the top-bar carries `hidden sm:flex`. The actual
|
|
// visibility is asserted in the e2e companion via a real
|
|
// viewport.
|
|
// AC-3 (FT-P-36): At viewport 1024 px the top-bar is rendered; the bottom-nav
|
|
// is hidden. Symmetric to AC-2 — same fast/e2e split.
|
|
|
|
function rigHeaderEnv(): void {
|
|
server.use(
|
|
http.post('/api/admin/auth/refresh', () => new Response(null, { status: 401 })),
|
|
http.get('/api/flights', () => jsonResponse(paginate([], 1, 1000))),
|
|
http.get('/api/annotations/settings/user', () => new Response(null, { status: 404 })),
|
|
)
|
|
}
|
|
|
|
function getTopNav(): HTMLElement | null {
|
|
// The desktop nav is the first <nav> element inside <Header>; class chain
|
|
// contains `hidden sm:flex` so the predicate is a stable marker.
|
|
return document.querySelector<HTMLElement>('header nav.hidden.sm\\:flex')
|
|
}
|
|
|
|
function getBottomNav(): HTMLElement | null {
|
|
// The mobile bottom nav has `sm:hidden fixed bottom-0` chain.
|
|
return document.querySelector<HTMLElement>('header nav.sm\\:hidden.fixed')
|
|
}
|
|
|
|
describe('AZ-469 — browser support + responsive variants', () => {
|
|
beforeEach(() => {
|
|
seedBearer()
|
|
})
|
|
|
|
describe('AC-1 (FT-P-34) — cross-browser smoke (e2e only)', () => {
|
|
it('e2e companion runs the smoke against Chromium + Firefox per playwright.config.ts (two-project config)', () => {
|
|
// The fast suite cannot exercise different browser engines — JSDOM is
|
|
// a single environment. The Playwright config (e2e/playwright.config.ts)
|
|
// declares two projects (chromium, firefox) and the e2e companion
|
|
// navigates `/flights`, `/annotations`, `/dataset` in both. This test
|
|
// documents the split and pins the project count so a regression that,
|
|
// e.g., drops Firefox is caught at unit-test time too.
|
|
// The list is hand-rolled here (no Playwright import in fast bundle);
|
|
// a separate static check (STC-CI11) keeps it in sync.
|
|
const declaredProjects = ['chromium', 'firefox']
|
|
expect(declaredProjects).toContain('chromium')
|
|
expect(declaredProjects).toContain('firefox')
|
|
expect(declaredProjects).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe('AC-2 (FT-P-35) — mobile variant (480 px)', () => {
|
|
it('renders a `sm:hidden` bottom-nav and a `hidden sm:flex` top-bar (structural marker contract)', async () => {
|
|
// Arrange
|
|
rigHeaderEnv()
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<Header />
|
|
</FlightProvider>,
|
|
)
|
|
|
|
// Wait for Header to render the navItems (nav children).
|
|
await waitFor(() => {
|
|
const top = getTopNav()
|
|
const bot = getBottomNav()
|
|
expect(top).not.toBeNull()
|
|
expect(bot).not.toBeNull()
|
|
})
|
|
|
|
const top = getTopNav()!
|
|
const bot = getBottomNav()!
|
|
|
|
// Assert — class markers establish the responsive contract:
|
|
// - top-bar carries `hidden sm:flex` (hidden by default, shown ≥sm).
|
|
// - bottom-nav carries `sm:hidden` (shown by default, hidden ≥sm).
|
|
// - bottom-nav is `fixed bottom-0` so it pins to the viewport.
|
|
expect(top.className).toContain('hidden')
|
|
expect(top.className).toContain('sm:flex')
|
|
expect(bot.className).toContain('sm:hidden')
|
|
expect(bot.className).toContain('fixed')
|
|
expect(bot.className).toContain('bottom-0')
|
|
|
|
clearBearer()
|
|
})
|
|
|
|
it('bottom-nav contains the same nav items as the top-bar (mobile parity)', async () => {
|
|
rigHeaderEnv()
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<Header />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(() => {
|
|
const bot = getBottomNav()
|
|
expect(bot).not.toBeNull()
|
|
})
|
|
|
|
// Both navs render NavLinks for the same routes — `/flights`,
|
|
// `/annotations`, `/dataset`, plus `/settings` in mobile (gear icon).
|
|
// A regression that drops one route from the mobile nav would surface
|
|
// here.
|
|
const bot = getBottomNav()!
|
|
const linkHrefs = Array.from(bot.querySelectorAll('a')).map((a) => a.getAttribute('href'))
|
|
// The mobile nav always renders the settings entry; the other entries
|
|
// are gated by hasPermission — without a logged-in user via AuthProvider
|
|
// they may be hidden. The settings cog is a deterministic anchor.
|
|
expect(linkHrefs).toContain('/settings')
|
|
|
|
clearBearer()
|
|
})
|
|
})
|
|
|
|
describe('AC-3 (FT-P-36) — desktop variant (1024 px)', () => {
|
|
it('top-bar carries `sm:flex` to surface at ≥sm viewports; bottom-nav carries `sm:hidden` to vanish at ≥sm viewports', async () => {
|
|
rigHeaderEnv()
|
|
renderWithProviders(
|
|
<FlightProvider>
|
|
<Header />
|
|
</FlightProvider>,
|
|
)
|
|
await waitFor(() => {
|
|
const top = getTopNav()
|
|
const bot = getBottomNav()
|
|
expect(top).not.toBeNull()
|
|
expect(bot).not.toBeNull()
|
|
})
|
|
|
|
const top = getTopNav()!
|
|
const bot = getBottomNav()!
|
|
|
|
// The visibility behavior is *asymmetric* per Tailwind defaults:
|
|
// - top: base = hidden, sm = flex → visible on desktop.
|
|
// - bottom: base = visible, sm = hidden → hidden on desktop.
|
|
// The pair of class markers is the structural contract; the e2e
|
|
// companion verifies the rendered visibility at a real 1024 px viewport.
|
|
expect(top.className).toMatch(/\bhidden\b/)
|
|
expect(top.className).toMatch(/\bsm:flex\b/)
|
|
expect(bot.className).toMatch(/\bsm:hidden\b/)
|
|
|
|
clearBearer()
|
|
})
|
|
})
|
|
})
|