Files
ui/tests/browser_support_responsive.test.tsx
T
Oleksandr Bezdieniezhnykh 23746ec61d [AZ-485] Add Public API barrels + STC-ARCH-01 (F4 close)
Closes architecture baseline finding F4. Every component now exposes
its Public API through `src/<component>/index.ts`; cross-component
imports go through the barrel. `scripts/check-arch-imports.mjs` plus
`STC-ARCH-01` in the static profile enforce the rule; tests in
`tests/architecture_imports.test.ts` cover AC-4/AC-5 + 2 exemption
cases. One F3-pending exemption (`classColors`) is documented in 5
places (barrel, consumer, script, doc, test) to avoid a circular
import.

Phase B cycle 1 batch 1 of 2 (epic AZ-447). Batch 2 is AZ-486
(endpoint builders) — blocked on this commit landing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 10:33:30 +03:00

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.get('/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()
})
})
})