Files
textbee/web/e2e/session.ts
isra el 0ea85c05e0 feat: clean up app chrome, rebuild footer and fix sidebar overlap
Top bar (app-header):
- Removed the Contribute button. It stays reachable from the footer.
- Removed the theme selector; it moves to the sidebar footer.
- Removed the mobile sheet for signed-in users: it duplicated the bottom tab
  bar for navigation and the avatar menu for identity. Signed-out visitors
  keep it, since auth pages have no tab bar.
The header is now brand on the left, account on the right.

Footer:
- Rebuilt as a slim single bar. A logged-in user is already converted, so the
  app does not need the marketing site's multi-column link farm. It borrows
  that footer's visual language (muted surface, muted-to-foreground hovers,
  green status pill) so the two still read as one product.
- Fixed the "cut by the side nav" bug. The footer was rendered in
  (app)/layout.tsx as a sibling of <main>, so it spanned the full viewport
  while the dashboard's fixed sidebar painted over its left 240px, and the
  fixed mobile tab bar covered its bottom edge. It is now rendered per
  section, inside each content column, with the dashboard copy padded clear
  of the tab bar.

Theme control:
- Moved to the sidebar footer and restyled from a dropdown into a three-way
  Light/Dark/System segmented control, which now has room to show all three
  states at once.

Sidebar icon:
- Dashboard uses LayoutDashboard instead of Home, matching the icon the
  avatar menu already uses for the same destination.

E2e harness, two real fixes found while verifying:
- The suite ran against `next dev`, which compiles routes on demand, so
  parallel workers hitting cold routes timed out at random. Switched to a
  production build; the suite went from 1.3m to ~31s.
- The survey modal (a Math.random() coin flip) and the update-app prompt
  (whenever a mocked device reports an old version) could open over any page.
  A Radix dialog marks the rest of the page aria-hidden while open, so every
  getByRole query found nothing and a different test failed each run. Both are
  now suppressed deterministically in the session helper.
Full suite: 16/16 on four consecutive runs.

New chrome.spec.ts guards the footer geometry against both the sidebar and
the mobile tab bar, asserts the top bar stays reduced, and asserts the
sidebar theme control actually toggles the dark class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:21:54 +03:00

59 lines
2.0 KiB
TypeScript

import type { BrowserContext } from '@playwright/test'
import { encode } from 'next-auth/jwt'
import { TEST_AUTH_SECRET } from '../playwright.config'
import { mockUser, TEST_ACCESS_TOKEN } from '../test/fixtures'
// NextAuth v4 default session cookie name over http (non-secure).
const SESSION_COOKIE = 'next-auth.session-token'
// Two modals can open unprompted over the dashboard: the survey modal (on a
// Math.random() coin flip) and the update-app prompt (whenever a mocked device
// reports an old app version). A Radix dialog marks the rest of the page
// aria-hidden while open, so whenever either fired, every getByRole query on
// that page found nothing and a random test failed. Seeding their dismissal
// flags suppresses both deterministically.
//
// Tests that want to assert on these modals should clear the flags themselves.
async function suppressInterruptingModals(context: BrowserContext) {
await context.addInitScript(() => {
window.localStorage.setItem('survey_modal_has_submitted', 'true')
// Snooze far enough out that the prompt stays closed for the run.
window.localStorage.setItem(
'update_app_prompt_snooze_until',
String(Date.now() + 365 * 24 * 60 * 60 * 1000)
)
})
}
// Mint a valid NextAuth JWT session cookie so middleware's getToken() accepts
// the request and the app treats us as authenticated, without ever running the
// real login flow or contacting a backend.
export async function authenticate(context: BrowserContext) {
await suppressInterruptingModals(context)
const token = await encode({
token: {
sub: mockUser.id,
id: mockUser.id,
name: mockUser.name,
email: mockUser.email,
phone: mockUser.phone,
role: mockUser.role,
accessToken: TEST_ACCESS_TOKEN,
},
secret: TEST_AUTH_SECRET,
})
await context.addCookies([
{
name: SESSION_COOKIE,
value: token,
domain: 'localhost',
path: '/',
httpOnly: true,
sameSite: 'Lax',
expires: Math.floor(Date.now() / 1000) + 60 * 60,
},
])
}