mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:21:11 +00:00
bd2b718ddf
- AZ-463 flight selection persistence (FT-P-16) + rehydration
on boot (FT-P-17) PASS at the wire; 100-cycle leak guard
(NFT-RES-LIM-07) and 1h SSE soak (NFT-RES-LIM-06)
scaffolded as RUN_LONG_RUNNING-gated e2e companions.
- AZ-469 browser-support smoke (FT-P-34) runs in both
Chromium and Firefox via the existing playwright config;
responsive variants (FT-P-35 480px / FT-P-36 1024px) PASS
in fast (Tailwind class shape) and e2e (visibility).
- AZ-476 upload 501 MB -> 413: AC-1 user-visible error is
drift today (uploadFiles silently falls through to local
mode); it.fails() + control + e2e test.fail. AC-2 no-alert
PASS via dialog spy.
- AZ-477 settings save 500 / network drop: AC-1+AC-2+AC-3
all drift today (no try/finally, no error region, deadline
unmeasurable); 4 it.fails() + control pinning the stuck-
disabled drift; e2e companions test.fail mirror it.
- LESSONS.md seeded: vi.stubGlobal('URL', {...URL,...})
destroys the URL constructor and breaks new URL(...) in
MSW; patch the methods directly instead.
Code review: PASS (0 findings). Fast: 22/22 files, 120
passed / 13 skipped. Static: 24/24 PASS.
Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
6.5 KiB
TypeScript
159 lines
6.5 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 } from '../src/components/FlightContext'
|
|
import Header from '../src/components/Header'
|
|
|
|
// 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()
|
|
})
|
|
})
|
|
})
|