freezmo/tests
Martin Dvorak 77819b5961 Push reminders: personal to the notes, generic without them
The daily reminder said one generic line to everyone, because the server
never sees a deck. Now the notification text is written in the browser
(lib/reminder.ts) and travels with the subscription:

- Personal: "3 cards are due in Sky deck", in the app's language, tapping
  through to /review. Without notes (or with nothing due) it is the plain
  nudge, translated in both languages — push.title / push.due.* / push.generic.
- POST /api/push accepts an optional message {title, body, url}; url must be
  a path on this app, so a stolen subscription cannot make the notification
  link elsewhere. The store keeps endpoint, keys, the message and createdAt
  (a refresh no longer resets the timestamp).
- components/PushReminderSync.tsx refreshes the stored text when the app
  opens and whenever the tab becomes visible (throttled to twice a day), so a
  count is only ever sent while it is true.
- scripts/send-reminders.mjs sends each device its own text and falls back to
  PUSH_REMINDER_TITLE/_BODY for a subscription that has none
  (scripts/reminder-payload.mjs, unit-checked).
- /api/push/test previews the real reminder with a "Test:" prefix.
- deploy/freezmo-reminders.{service,timer}: the daily send as a systemd user
  timer (18:00 local, Persistent, randomised), documented in the README.
- Settings copy, privacy page (en+cs) and .env.example say exactly what the
  server now holds.
- phase2-test grew the whole path with a faked browser subscription: personal
  text in en and cs, the generic fallback in both, the URL guard, and the
  sender's preference order (81 checks).

Version 0.8.2, SW v104.
2026-10-18 14:34:15 +00:00
..
activities-test.mjs activities-test: widen the fresh-budget window for loaded machines 2026-10-16 05:43:37 +00:00
aero-music-test.mjs Aero music: the button works on the podcast player 2026-10-16 21:04:39 +00:00
bubble-perf.mjs Stop blurring the animated scene on static glass (2x frame time) 2026-10-12 19:47:36 +00:00
bubbles-test.mjs The scene stops heating the MacBook 2026-10-15 19:42:29 +02:00
chat-test.mjs Aero glass goes back to glass 2026-10-15 09:37:26 +00:00
contrast.mjs Podcast mode: notes read aloud with a lyric-style word highlight 2026-10-16 11:42:15 +00:00
freeze-loading-test.mjs Move the e2e suites into the repo (tests/), out of /tmp 2026-10-11 15:11:06 +00:00
heat-probe.mjs The scene stops heating the MacBook 2026-10-15 19:42:29 +02:00
i18n-test.mjs Follow the copy pass: tests, docs, version, SW 2026-10-14 20:22:12 +00:00
lib.mjs Podcast voices: natural provider TTS, and system-voice hygiene 2026-10-16 16:34:46 +00:00
mobile-test.mjs Chat chrome: one gutter, one radius scale, one even glass recipe 2026-10-14 21:32:50 +00:00
model-compare.mjs Chat mode, phase 1: grounded streaming chat over your notes 2026-10-11 18:32:11 +00:00
native-test.mjs Follow the copy pass: tests, docs, version, SW 2026-10-14 20:22:12 +00:00
phase1-test.mjs Deck/homework writes sync themselves when sync is on 2026-10-12 16:40:49 +00:00
phase2-test.mjs Push reminders: personal to the notes, generic without them 2026-10-18 14:34:15 +00:00
phase3-test.mjs Aero button glass + homework report control; reduced-motion sheet glitch 2026-10-14 09:19:54 +00:00
podcast-test.mjs podcast-test: no this-alias in the speech stub (the gate lints tests too) 2026-10-16 21:47:12 +00:00
providers-test.mjs Key fields: keep the browser's password UI away, fix the keyboard layout 2026-10-16 10:12:20 +00:00
README.md Podcast mode: notes read aloud with a lyric-style word highlight 2026-10-16 11:42:15 +00:00
regression.mjs Follow the copy pass: tests, docs, version, SW 2026-10-14 20:22:12 +00:00
review-test.mjs Review breakdown: show each deck's own activity total 2026-10-15 21:20:53 +00:00
run-all.mjs Teach it back: the AI becomes the curious student 2026-10-16 23:05:24 +00:00
security-test.mjs Security: model budget + concurrency caps, SSRF guards, spoof-proof limits 2026-10-12 20:40:57 +00:00
teach-test.mjs Teach it back: the AI becomes the curious student 2026-10-16 23:05:24 +00:00
wizard-test.mjs Podcast mode: notes read aloud with a lyric-style word highlight 2026-10-16 11:42:15 +00:00

Browser suites

End-to-end checks for Freezmo, driven by Playwright against a running production build. Everything lives in this repo (nothing in /tmp), so a reboot cannot wipe the harness.

Requirements

  • The app is running on http://127.0.0.1:3207 (systemctl --user start freezmo).
  • A Chromium build. The suites auto-detect the newest Playwright Chromium in ~/.cache/ms-playwright/ (Linux) or ~/Library/Caches/ms-playwright/ (macOS); override with CHROME=/path/to/chrome.
  • npm install (dev deps: playwright-core, axe-core).

Running

npm run build && systemctl --user restart freezmo   # fresh build first
npm run test:e2e                 # every suite, in order
npm run test:e2e -- phase1       # only suites whose name contains "phase1"

Each suite prints PASS/FAIL lines and exits non-zero on failure; the runner prints a per-suite summary at the end.

Suites

Suite Covers
regression.mjs Library, deck card + menu, study feed to results, homework entry, desktop shell + settings/health panel.
aero-music-test.mjs Frutiger Aero's soundtrack player (the SoundCloud widget API is stubbed): nothing loads in light/dark, a random playlist track starts in Aero, the circle expands into the credit bar, pause/collapse/close wiring, the off choice persisting and Aero re-selecting re-arming it, phone geometry over the tab bar and the chat composer, and the control stepping aside under an immersive overlay.
bubbles-test.mjs Ice-scene bubbles: stacking/layout contract, the open band (a plain clip, no mask) and a 41-phase sweep that no visible orb or bubble ever touches its edges, keyframes only transform+opacity, rise/sway/scale/fade phases, per-breakpoint counts (5/3/2), phone pacing, reduced motion, parking (hidden tab, unfocused window, dialog scrim), and the cost contract (static blobs, no blend-mode grain, no blur on static glass, no hidden blur under Aero's bake).
mobile-test.mjs Five-tab bar at 360/390 × EN/CS × dark/light × reduced motion (labels, targets, no hamburger/drawer), the library ⋯ sheet (Import JSON / New folder), the settings sheet (touch targets, key flow, drag-to-close, backdrop), axe contrast audits.
wizard-test.mjs Deck 3-step flow and homework flow, flat action bar, in-page mode switch, the loading lock, abort-on-leave.
native-test.mjs Manifest/PWA head, offline banner + disabled generate, share target GET + POST, Back-gesture layers, undo toast, tab re-tap, haptics, draft restore.
i18n-test.mjs EN/CS dictionary parity, locale switch, Czech desktop shell, Czech phone tab labels and plurals.
freeze-loading-test.mjs The generation loading screen appears immediately, hides the action bar, Cancel returns with the notice.
podcast-test.mjs Podcast mode against a stub Ollama and a stubbed speech engine: the third wizard segment, notes → a saved episode, the word-wrapped transcript, boundary-driven highlighting (and the timer fallback's contract), pause/resume-from-word, chapter jumps, speed changes that keep playing, the position saved on page-hide and resumed on reopen, the library tab, and the API's 400/stream guards.
review-test.mjs Smart Review loading feedback under a stalled request, the session end-to-end, mistakes, and the health-probe cache.
phase1-test.mjs Backup export/import, persistence row, nudge, encrypted sync (enable/join/push/delete + API validation), usage-limit handling (client + a stub Ollama).
phase2-test.mjs Answer history + SRS from a real study run, review/mistakes, stats, exam mode, push reminders hidden without VAPID and validated with it.
phase3-test.mjs Check my work (API + phone/desktop UI), follow-ups, guide step explain, turn-into-a-study-deck.
chat-test.mjs Chat: streaming API (validation, allowlist, context travel, history window, thinking separation, usage-limit mapping), attachment extraction, entry points, streaming/stop, markdown + KaTeX, model/scope pickers, edit/regenerate, offline, reduced motion, plus history (IDB persistence, reload, rename/search/delete/undo, scope+model memory), assistant actions, the export payload and a two-device sync round-trip.

Extra instances

phase1, phase2 and phase3 start their own next start instance with a stub Ollama (or VAPID keys) on a spare port (3208/3209). Each suite checks the port is free first and kills the whole process group on exit, so ports do not leak between suites. Scratch data for the VAPID test lives in tests/.tmp/ (gitignored).

Diagnostics (not part of the run)

node tests/bubble-perf.mjs [label] measures the ice-scene frame cost with a CDP performance trace (rAF frame timing, long tasks, main-thread busy time, style recalcs) on a 4× throttled phone profile and a 2× desktop profile. It always exits 0; the numbers are for tuning the bubble count/speed.

node tests/heat-probe.mjs [webkit|chromium] (macOS only) measures what an idle screen actually costs the machine: headed Playwright WebKit and Chromium on the real display, the browser's CPU, the GPU time of its processes and of WindowServer, per screen and theme, against an about:blank baseline and with the scene parked. Install the engines once with npx playwright-core install webkit chromium and keep the window in front.