# padhle — Frontend Manual Testing Plan > **Goal:** test the application through the browser (frontend only) and verify every feature that was built, changed, or hardened. > **Setup:** backend on `:3001`, frontend dev server on `:5173`, local Supabase running (`npx supabase start`). > **Recommended browsers:** test in a **normal window** and an **incognito window** side-by-side (incognito = clean anonymous identity). --- ## 0. Before you start — launch | Step | Do this | Expected | |---|---|---| | 0.1 | Terminal 1: `cd backend && npm run dev` | Backend on `:3001` | | 0.2 | Terminal 2: `cd frontend && npm run dev` | Frontend opens `http://localhost:5173` | | 0.3 | Open `http://localhost:5173` in incognito | App loads; **no sign-in wall** — you're in the app (sidebar, selectors, chat input visible) | > ⚠️ **If you don't have a Supabase/backend running**, backend calls for sign-in/chat will fail with a connection error banner. The anonymous flow also needs the backend for chat. If a *real* AI key has no quota, streams may end with an in-chat error (expected, not a code bug). --- ## 1. Anonymous 5-message trial (FREE-TIER — this is the headline feature) > This is the core new behavior. Everything here works **without** an account. | # | Do this | Expected | |---|---|---| | 1.1 | In incognito, notice the TopNav + Sidebar | Both show a **"Sign in"** button (no avatar) | | 1.2 | Pick a grade → subject → chapter, then send a message | Streaming answer appears; chat thread builds | | 1.3 | Send 4 more messages (5 total) | Each works; counter quietly builds up (you see the conversation continue) | | 1.4 | Send a **6th** message | Composer is **replaced** by a card: *"You've used your 5 free messages — Sign in to continue"* | | 1.5 | If you've closed/reopened between messages: the URL stays on `/` the whole time | Anonymous chat does **NOT** reflect a chat id into the browser URL | | 1.6 | Click **"Sign in to continue"** on the card | The sign-in modal opens | | 1.7 | Close the modal (× / "Maybe later" / click the dimmer) | Modal closes; you can keep navigating, still capped at 5 | | 1.8 | **Refresh the page** after using some messages | Conversation resets to a **fresh** 5-message budget (by design) | | 1.9 | **Secure:** open DevTools → Application → Cookies | There is **no** `padhle.anon` cookie; your anonymous state is 100% in-memory (refresh wipes it) | **Cross-browser isolation:** | # | Do this | Expected | |---|---|---| | 1.10 | Use 2 different browsers (or normal + incognito), both anonymous | Each has its **own separate** budget; their chats are **not** visible to each other | --- ## 2. Sign-up / Sign-in / Auth | # | Do this | Expected | |---|---|---| | 2.1 | Click **"Sign in"** (TopNav or Sidebar) | Modal opens cleanly | | 2.2 | **Sign up** with a valid email + password (≥6 chars) | Account created; you are now signed in; avatar appears in TopNav; sessions sidebar appears | | 2.3 | **Reload the page** (`F5`) | You stay signed in (session restored from the httpOnly cookie) | | 2.4 | Try to **sign up with a 5-character password** (via the UI if it lets you, or note): | Server must reject (password ≥ 6). The UI enforces it too — if the UI blocks it, that's fine | | 2.5 | **Sign out** (Sidebar → sign out) | You are logged out; back to anonymous app with a **fresh** 5-message budget; "Sign in" buttons return | | 2.6 | **Sign back in** with the same email/password | Your previous signed-in conversation is still there in the sidebar | --- ## 3. Sessions (persisted conversations, signed-in only) | # | Do this | Expected | |---|---|---| | 3.1 | While signed in, start a new chat and send a few messages | When you send, the URL becomes `/chat/` (signed-in chats ARE deep-linkable) | | 3.2 | Open another chat from the sidebar | URL switches to that chat's `/chat/`; back/forward browser buttons work | | 3.3 | Copy the `/chat/` URL, open it in **another tab** (signed in there) | The conversation loads directly (deep link works) | | 3.4 | **New chat** button | Resets to `/`, clears composer, back to a fresh conversation | | 3.5 | **Delete a session** from the sidebar | Removed; no longer in the list | | 3.6 | **Clear messages** in a session (if available) | Conversation empties but the session stays | | 3.7 | **Secure — ownership:** in browser A (user 1) note a session URL; in incognito (user 2) paste that exact URL | Two possibilities: if you are NOT signed in there → app shows clean "could not load" / stays on `/`; if you ARE signed in as user 2 → NOT user 1's messages (must be 404 — you should NOT see user 1's chat) | --- ## 4. Routing / URLs (added this session) | # | Do this | Expected | |---|---|---| | 4.1 | While **anonymous**, send a message | URL **stays `/`** (anonymous trial id is never written to the URL — security decision) | | 4.2 | While **signed in**, open a chat | URL is `/chat/` — shareable and refreshable | | 4.3 | Type a random path, e.g. `http://localhost:5173/whatever` | App still loads at the root shell (SPA fallback) — no 404 page yet (documented) | | 4.4 | Browser **back/forward** buttons between chats | Moves between conversations as expected | --- ## 5. AI streaming + rendering | # | Do this | Expected | |---|---|---| | 5.1 | Send a question and watch the reply | Answer **streams** word by word (token by token), typing indicator during | | 5.2 | Ask for something with **Markdown** (e.g. "give me a bulleted list, bold text, a code block") | Renders as formatted Markdown (not raw `**` / backticks) | | 5.3 | Ask for **math** (e.g. "what is `x^2 + y^2`") | LaTeX renders via KaTeX | | 5.4 | **Secure — XSS:** ask the AI to output raw HTML like `` (or intentionally craft a message) | It renders as **text**, NOT executable HTML — no alert pops | | 5.5 | Long answer | Auto-scrolls to the new content | --- ## 6. Errors, validation, robustness (secure boundary checks) > These verify the backend still protects you even if the frontend is bypassed. Easiest way: open DevTools Console and run `fetch(...)` from the page (same-origin) — or just reason through the UI where possible. | # | Try | Expected | |---|---|---| | 6.1 | `fetch('http://localhost:3001/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:123})})` (no session cookie) | **400** with a clean JSON error — NOT a 500 / stack trace | | 6.2 | `fetch('http://localhost:3001/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:'hi',chapter:'