Commit Graph

7 Commits

Author SHA1 Message Date
xarmian 954c84d0bf refactor(e2e): extract demo data into shared module (TASK-1201) (#431)
Lift the static "realistic workspace" data out of seedRealisticContent
into web/e2e/lib/demo-data.ts so it can be consumed by pad-remotion (a
sibling repo) without dragging in Playwright as a dependency. Single
source of truth for what a real-feeling Pad demo looks like.

Wire-shape compatibility is preserved exactly:
- demoPlan: same title, status, content
- demoTasks: same 7 tasks in same order, same status/priority/effort,
  same parent-to-plan linkage (now expressed via parentToPlan: boolean
  rather than carrying the plan id inline — the seeder maps it back
  to the freshly-created plan id at post time)
- demoIdeas: same 2 ideas, same fields

Behavior verified by running the gated screenshot spec:
  PAD_SCREENSHOTS=1 npx playwright test e2e/screenshots.spec.ts \
    --project=desktop-chromium
seedRealisticContent runs to completion and the dashboard / board /
list / table screenshots regenerate identically (reverted; not part
of this PR's diff).

demoConventions is also exported (4 representative entries) for the
pad-remotion ContextScene to render ghost-cards. demo-seed.ts itself
doesn't consume it — seedConventions takes caller-supplied input — but
the shape lives here so the shared data module is complete.

The companion pad-remotion file (src/data/demoItems.ts) lands as a
separate PR in PerpetualSoftware/pad-remotion. We chose copy-with-
manual-sync over a path import / symlink because pad-remotion is a
distinct git repo; a CI drift check is a possible follow-up if this
duplication starts to bite.

Parent: PLAN-1198.
2026-05-07 10:32:52 -04:00
xarmian cf04e16b5e chore(e2e): blog screenshot capture spec + shared seed helpers (TASK-1031) (#374)
Adds infrastructure for capturing Pad UI screenshots that ship inside
blog posts on getpad.dev.

* web/e2e/lib/demo-seed.ts (new) — extracts the realistic-content seed
  (1 active plan + 7 tasks + 2 ideas) from screenshots.spec.ts into a
  shared module, plus two new helpers:
    - seedConventions(fixture, request, [...])
    - activateLibraryConventions(fixture, request, titles)
  Both consumers now share the same source of truth.

* web/e2e/blog-screenshots.spec.ts (new) — gated on
  PAD_BLOG_SCREENSHOTS=1. One test.describe per blog post; each owns
  its post-specific seed and captures into ../../pad-web/static/blog/
  <slug>/. First consumer is BLOG-1007 (Conventions and Playbooks);
  subsequent posts add a describe block per shot.

* web/e2e/screenshots.spec.ts — refactored to import seedRealisticContent
  from the shared lib. No behavior change; PAD_SCREENSHOTS=1 README
  capture still passes.

Companion publish helper lives in pad-web at scripts/blog-publish.mjs.

Capture command:
  make build-go && cd web && PAD_BLOG_SCREENSHOTS=1 \
    npx playwright test blog-screenshots --project=desktop-chromium

Refs TASK-1031, unblocks BLOG-1022 / BLOG-1004 / BLOG-1003 backfill
which all want screenshots.
2026-05-02 13:05:55 -04:00
xarmian 94ebe5a83d test(screenshots): capture in dark mode (Pad's default theme) (#319)
The README screenshot capture script ran in light mode because
Playwright's headless Chromium reports prefers-color-scheme: light by
default. The Pad layout's onMount logic explicitly forces
data-theme="light" when matchMedia matches 'light' — so the captures
came out light-themed even though Pad defaults to dark when no user
preference exists.

Two effects made this misleading:
1. README screenshots showed a theme most Pad users never see by
   default. The first impression in the README didn't match the
   first impression of the running app.
2. The screenshots could not be reused in the getpad.dev marketing
   site (dark themed) without visible whiplash. TASK-918 (PLAN-911)
   needs them on the homepage; light-mode captures would have looked
   like screenshots of some other product.

Fix: pass colorScheme: 'dark' via test.use(). Chromium then reports
prefers-color-scheme: dark to the page; the layout's matchMedia check
no longer matches 'light', so it leaves the document on the default
theme — which is dark.

Also fixed a typo in the re-run instruction in the docstring (the
PAD_SCREENSHOTS=1 env var was attached to the wrong command).

Re-captured all three screenshots (dashboard, board, list) under the
new config. Docstring updated to call out the theme rationale so
future maintainers don't accidentally flip it back.
2026-04-30 13:10:51 -04:00
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
xarmian 272868291c test(e2e): web export → import round-trip with attachment (TASK-894) (#310)
Closes PLAN-890's regression-safety net: a Playwright spec that
seeds a source workspace with an item embedding a real PNG
attachment, drives the bundle export through the settings page,
imports it through the Create Workspace modal, and verifies via the
API that the imported workspace carries the item, the rewritten
attachment reference, and the rehydrated blob with bytes matching
the original upload.

Failure modes the spec catches:

- Export link reverts to JSON (toHaveAttribute on href + download)
- Import dispatch silently routes to the legacy JSON path (server
  would fail with gzip decode)
- Attachment id rewrite regresses (asserts the new content does
  NOT contain the OLD UUID and DOES contain a fresh UUID)
- Storage/rehydrate path corrupts bytes (assert downloaded blob
  bytes equal the original PNG)

Implementation notes:

- Bundle bytes are fetched via the auth'd `request` fixture, not
  via the browser's `<a download>` click. Playwright's download
  fork doesn't carry extraHTTPHeaders (Bearer token), so the
  click would 401. The link's href + download attribute are still
  asserted via toHaveAttribute — that pins the export-side UI
  contract.
- Modal is opened via a dual-path selector: TopBar's "+ New
  workspace" button when visible, falling back to the
  WorkspaceSwitcher dropdown's "+ New Workspace" entry. Same
  uiStore.createWorkspaceOpen flag, same modal — different chrome
  on different viewports.
- Spec is pinned to desktop-chromium. Running both projects in
  parallel trips the server's general-API rate limit
  (~10 req/sec) because seed + export + import + verify makes
  ~30 calls per worker. The flow has no viewport-specific code
  worth covering twice; one project is sufficient for round-trip
  integrity. Documented in the inline `test.skip`.
- 1x1 PNG byte sequence is the same as Go's realPNG() — same
  bytes the server-side attachment tests use, so the e2e
  exercises the same MIME-validation path.

Parent: PLAN-890.
2026-04-29 20:49:37 -04:00
xarmian 29f720c996 docs: add real README screenshots (dashboard + board views) (#257)
The README had two TODO placeholders for screenshots that have been
sitting commented-out since the project started. With the launch
imminent, fill them in.

Captures:
- docs/screenshots/dashboard.png — workspace dashboard with Active
  Work cards, Active Plans (v0.2 — Collaboration with progress),
  collection summaries, recent activity.
- docs/screenshots/board.png — tasks board view, four columns
  (Open / In-Progress / Done / Cancelled) with realistic task cards.
- docs/screenshots/list.png — list view (not currently referenced
  from the README, but kept as part of the reproducible asset set).

Reproducibility:

web/e2e/screenshots.spec.ts is a gated Playwright spec (skipped
unless PAD_SCREENSHOTS=1) that uses the existing e2e fixture
infrastructure to:
1. Spin up a fresh pad binary against a clean data dir.
2. Bootstrap an admin + workspace seeded with the startup template.
3. Add a realistic demo dataset (1 active plan, 7 tasks across
   open/in-progress/done with mixed priorities, 2 ideas).
4. Navigate + capture three views at 1440x900.

To regenerate:

  make build
  cd web && PAD_SCREENSHOTS=1 PAD_E2E_PORT=17801 \\
    npx playwright test screenshots --project=desktop-chromium

Notes:

- Table view (?view=table) was originally in scope but the URL
  parser only accepts list/board today; setting via toggle would
  require localStorage manipulation. Three screenshots already
  cover the README's needs; revisit if/when table view becomes
  URL-reachable.
- Dark/light variants were also in scope but the web UI is dark-
  mode-only at present, so the captures are dark-only.

Refs: TASK-673
2026-04-26 20:12:40 -04:00
xarmian c8601a2031 test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689) (#225)
* test(e2e): Playwright smoke test infrastructure + 2 dashboard tests (TASK-689)

Option A of TASK-689: land the test infrastructure and a minimal smoke
test on both mobile and desktop viewports. Broader flow coverage (board
view drag, item detail, comments, mobile hamburger, BottomSheet
regression guard) is tracked as TASK-733.

Infrastructure
--------------
- web/playwright.config.ts: two projects (desktop-chromium, mobile-
  chromium via Pixel 7), reporter list+html, trace/video/screenshot
  retained on failure, webServer that wipes + recreates the data dir
  then runs the pad binary. Paths anchored to the config file's
  directory so runs are cwd-invariant.
- web/e2e/global-setup.ts: bootstraps admin via POST /auth/bootstrap,
  logs in, creates the e2e workspace, mints a user-scoped API token,
  and persists the token + resolved admin username to fixture.json.
- web/e2e/fixtures.ts: extends base test so every BrowserContext
  automatically gets Authorization: Bearer <token>. Uses a token
  rather than a session cookie because sessions are User-Agent bound
  in middleware_auth.go and a node-minted session would be rejected
  by a Chromium UA.

Tests
-----
- web/e2e/dashboard.spec.ts: a logged-in user lands on the seeded
  workspace, no login form is rendered, and the workspace name
  appears on the page. Runs in both project viewports.

CI
--
- New `e2e` job in .github/workflows/ci.yml: builds web UI + binary,
  installs Playwright chromium with OS deps, runs the suite, and
  uploads the HTML report as an artifact on failure. Timeout capped
  at 10 minutes (suite itself runs in ~4s today).

Local run (in mcr.microsoft.com/playwright:v1.59.1-noble): 2 passed
in 4.1s.

Parent: PLAN-644.
Follow-up: TASK-733 for broader flow coverage (Option B in the
original ship plan).

* fix(e2e): persist server-returned workspace slug instead of the constant (TASK-689)

Addresses Codex P2 on PR #225. When Playwright's `reuseExistingServer:
true` (local dev), a re-run of globalSetup hits `POST /api/v1/workspaces`
against a DB that already has `e2e`. The server uniquifies the slug
(`e2e` → `e2e-2` → …) and returns the uniquified value, but the old
code wrote `WORKSPACE_SLUG` (the constant) to fixture.json. Tests
then navigated to /e2e-admin/e2e — which might still exist from a
previous run with stale state — instead of /e2e-admin/e2e-2, missing
regressions in freshly-seeded content.

Fix: read `slug` back from the workspace-create response and use that
when writing fixture.json. Local re-runs now always point at the
workspace this run actually created.

Parent: PLAN-644.

* fix(e2e): cross-platform webServer bootstrap via Node wrapper (TASK-689)

Addresses Codex P2 on PR #225: `rm -rf && mkdir -p && pad server start`
in webServer.command is POSIX-only. Windows contributors on cmd.exe or
PowerShell can't run `npm run test:e2e` at all — the e2e suite becomes
Linux/macOS-only, defeating the "CI parity" goal.

Fix: extract the wipe-and-exec logic into web/e2e/run-pad.mjs. Node's
fs.rmSync / mkdirSync / child_process.spawn are uniform across
platforms, and the wrapper forwards SIGTERM/SIGINT so Playwright's
teardown still cleanly kills the child on suite exit.

Local re-run in mcr.microsoft.com/playwright:v1.59.1-noble: 2 passed.

Parent: PLAN-644.
2026-04-22 22:35:08 -04:00