Files
pad/web/e2e
xarmian 8f2be1b391 feat(error): branded 404 + 500 error pages with cloud-mode chrome (TASK-906) (#315)
* feat(error): branded 404 + 500 error pages with cloud-mode chrome (TASK-906)

New web/src/routes/+error.svelte renders for any unhandled error or
unmatched route in the SvelteKit tree. Friendly status-specific titles
+ hints (404, 401/403/500 covered explicitly; falls through to a
generic "An error occurred" + framework message for anything else).

Cloud mode wraps the error in marketing chrome — AuthHeader at the
top, AuthFooter at the bottom — and adds two extra escape CTAs ("Back
to getpad.dev" + "Open docs") in addition to the always-present "Go to
home" button. So a 404 doesn't drop the user out of the brand and they
always have somewhere to go.

Self-hosted (cloudMode=false) renders a minimal centered card with
just the status code, friendly title/hint, the inline Pad wordmark
(matching the auth-card pattern), and a single "Go to home" CTA. No
getpad.dev branding imposed on operators' deployments — same gating
philosophy as TASK-902/903.

The page hydrates authStore in onMount so cloudMode resolves on first
paint, fire-and-forget; if the session fetch fails we render the
self-hosted variant — safe fallback.

Reuses AuthHeader and AuthFooter from TASK-902/903; no need for
hand-rolled chrome since those components landed first.

Parent: PLAN-900.

Test plan:
- web/npm run check — 0 errors (693 files; +1 from new page)
- web/npm run build — clean
- Svelte autofixer — clean

* fix(error): context-aware chrome for in-app vs marketing routes per Codex (round 2)

Codex caught that +error.svelte unconditionally rendered the Cloud
marketing AuthHeader/AuthFooter, but the root +layout.svelte already
wraps workspace pages in the Sidebar/TopBar/main-content app shell.
On a workspace 404 the result would be both chromes stacked: app
shell underneath plus a fixed-position marketing header floating
over the top.

Fix: branch on the same paths the root layout uses to decide whether
to render bare children. "Marketing context" (auth/share/console
paths) keeps the full Cloud-mode AuthHeader + AuthFooter treatment;
"app-shell context" (everything else, i.e. workspace pages) renders a
minimal centered block inside the existing main-content area with no
fixed-position chrome of its own.

This means the user-facing experience in each context is correct:

- /this-does-not-exist (no auth): Cloud → branded marketing 404;
  self-hosted → minimal centered card with Pad wordmark
- /login → same (auth-page family)
- /[user]/[ws]/some/missing/route: workspace shell stays intact
  with a centered "Page not found" inside the main-content area

The marketing-context list mirrors the bare-render condition in
web/src/routes/+layout.svelte (isAuthPage || isSharePage ||
isConsolePage) plus the share-page prefix.

Verification: npm run check 0 errors; web build clean.

* test(e2e): wait for workspace heading before probing topbar trigger

The bundle-roundtrip test fired a synchronous isVisible() check on
the desktop topbar trigger immediately after `domcontentloaded`. The
workspace shell is fully client-rendered (adapter-static has no SSR
for app routes), so isVisible() raced hydration: on slower CI runners
the topbar wasn't in the DOM yet, the check returned false, the test
fell through to the mobile branch, and it then timed out waiting for
an element that doesn't exist on the desktop-chromium project.

Surfaced by TASK-906 (this PR), which adds ~8 KB of root-level JS
(error page + AuthHeader/AuthFooter chunks). That extra chunk-loading
shifted the hydration race past the test's check on GitHub Actions
runners; it had been winning consistently before. Locally the test
passes in ~4s either way — the race is real but tight.

Anchor the wait on the workspace heading ("E2E Workspace") which the
dashboard route renders the moment hydration completes. Keeps the
existing desktop/mobile branching intact and adds one toBeVisible()
gate so the rest of the flow runs against a fully-hydrated UI on any
runner speed.

Verified locally: 4.0s pass after the change.
2026-04-29 23:46:07 -04:00
..