diff --git a/e2e/mobile-viewports.spec.ts b/e2e/mobile-viewports.spec.ts index fcbfbb51d..721f38292 100644 --- a/e2e/mobile-viewports.spec.ts +++ b/e2e/mobile-viewports.spec.ts @@ -47,6 +47,17 @@ async function gotoSettled(page: Page, route: string): Promise { // Reduced motion stabilizes geometry: entrance animations otherwise leave // elements mid-transform when we measure. await page.emulateMedia({ reducedMotion: 'reduce' }); + // The seeded storageState pins `sb_last_activity` (SESSION_IDLE_COOKIE in + // src/lib/auth/session-idle-shared.ts) at auth-setup time, so any test that + // starts more than SESSION_IDLE_TIMEOUT_MS (5 min) after setup is bounced + // to /login by the middleware idle gate before it can measure anything — + // the suite passes or fails on how close its runtime sits to that cliff. + // A viewport test simulates an ACTIVE user, so refresh the marker the same + // way real interaction does. + const baseURL = test.info().project.use.baseURL ?? 'http://localhost:3000'; + await page.context().addCookies([ + { name: 'sb_last_activity', value: Date.now().toString(), url: baseURL }, + ]); await page.goto(route, { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {}); await page.waitForTimeout(250); diff --git a/src/app/baseball/(dashboard)/_components/hub-sub-nav.tsx b/src/app/baseball/(dashboard)/_components/hub-sub-nav.tsx index c0ce608c6..5806ffe8b 100644 --- a/src/app/baseball/(dashboard)/_components/hub-sub-nav.tsx +++ b/src/app/baseball/(dashboard)/_components/hub-sub-nav.tsx @@ -257,23 +257,24 @@ export function HubSubNav({ tabs, ariaLabel, className }: HubSubNavProps) { const isActive = t.id === resolvedId; const Icon = t.icon; return ( - // min-w-0 (not shrink-0): #905 — at 320/390px a hub capped at - // ≤3 tabs (Ruling 2) can still exceed the viewport once icon + - // padding + a longer label ("Postgame Review", "Operations") - // are summed (e.g. Stats & Performance's 3 tabs measure to - // ~424px unshrunk at a 390px viewport). `shrink-0` gave every - // tab the flex item's default `min-width: auto` floor (its - // full unbreakable `whitespace-nowrap` label width), so the - // LAST tab's own bounding rect — not just the scrollable - // strip's content — bled past the viewport edge even though - // `overflow-x-auto` visually clipped it (getBoundingClientRect - // reflects layout position, not ancestor clipping). `min-w-0` - // restores the default `flex-shrink: 1` floor to zero so the - // label's `truncate` (below) can actually engage — tabs now - // shrink-to-fit inside the viewport first, falling back to the - // strip's horizontal scroll only once even truncated tabs - // don't fit (mirrors the FairwayBottomNav min-w-0 fix, #899). -
  • + // flex min-w-0 (not shrink-0): #905 — at 320/390px a hub capped + // at ≤3 tabs (Ruling 2) can still exceed the viewport once icon + // + padding + a longer label ("Postgame Review", "Operations") + // are summed. `min-w-0` zeroes this flex item's shrink floor, + // but that alone is HALF the mechanism: without `flex` the li + // stays display:list-item, so the anchor inside is never a flex + // item — it keeps its natural `whitespace-nowrap` width, the + // label's `truncate` never gets width pressure, and the li + // shrinking just lets the anchor overflow it (CI forensics: + // strip scrollWidth 325 vs clientWidth 320 at a 320px viewport + // with fonts loaded — earlier green runs measured before the + // webfont swap and passed by a few px of fallback-font luck). + // `flex` makes the li the anchor's flex container so the + // anchor's own min-w-0 (below) lets the span truncate — tabs + // shrink-to-fit first, falling back to the strip's horizontal + // scroll only once even truncated tabs don't fit (mirrors the + // FairwayBottomNav min-w-0 fix, #899). +
  • { @@ -295,7 +296,10 @@ export function HubSubNav({ tabs, ariaLabel, className }: HubSubNavProps) { // on specificity and neutralizes it; the global rule is // now also scoped to exclude `nav`-shaped anchors (this // strip is a `