super-productivity/e2e/store-screenshots
Miklos 67d1e32ffc
feat(focus-mode): focus screen UX overhaul (#7586)
* feat(focus-mode): focus screen UX overhaul

Major rework of the focus mode timer screens (#7349):

- Shared <focus-clock-face> drives Pomodoro / Flowtime / Countdown /
  Break with a single visual chrome; size tokens scale fluidly via
  clamp() + vmin/vh, no discrete breakpoints.
- Single source of truth: --clock-time-size and --control-offset
  derive from --clock-face-size.
- Countdown: click-to-edit duration, draggable handle on the ring,
  hybrid 5-min/15-min snap with 6h-per-rotation above 1h
  ([useFlexibleIncrement] on input-duration-slider, opt-in for other
  consumers).
- Pomodoro prep inherits the click-to-type input; drag handle hidden
  so dragging the ring can't silently shift the value.
- Pause keeps the selected task on screen (displayedTask falls back
  to the paused task while currentTask is null).
- Notes panel acts as a modal: clock stays put, backdrop dims and
  closes on outside-click.
- Session controls row below the circle (pause / complete / reset
  cycles); buttons fade via shared --revealed-opacity gated on host
  hover + document.hasFocus().
- Break screen mirrors focus-mode-main layout; cycle counter inside
  the circle on both focus and break; back-to-planning unified.
- Flowtime settings dialog: all fields render with proper
  disable/enable, stable dialog width when switching modes.
- arrow_backward -> arrow_back: fixes glitched glyph in repeat-type
  context menus (boards, simple counters, take-a-break, flowtime).

* fix(focus-mode): silence naming-convention lint on formly 'props.disabled'

Formly's expressionProperties path-string keys ('props.disabled') aren't
camelCase and the rule has no requiresQuotes exemption; matches the
existing pattern in src/app/features/issue/common-issue-form-stuff.const.ts.

* test(focus-mode): mock pomodoroConfig signal on FocusModeService

The component's initialization effect now reads
focusModeService.pomodoroConfig() in Pomodoro+Preparation mode, but the
three mocked FocusModeService instances in the spec didn't provide it,
causing TypeError in 47 tests on CI (somehow not surfaced locally).

* test(focus-mode): align specs with unified back-to-planning flow

- focus-mode-break.spec: exitBreakToPlanning -> cancelFocusSession
- focus-mode-session-done.spec: drop obsolete hideFocusOverlay assertion
  (cancelFocusSession now handles both clearing tracking and hiding the
  overlay, matching the production component)
- focus-mode-main.spec: storeSpy gains selectSignal returning a signal,
  needed by the displayedTask paused-task fallback

* fix(focus-mode): restore E2E selector hooks on refactored controls

The shared <focus-clock-face> refactor moved pause/complete buttons out
of the clock face into a new .circle-controls row but didn't carry the
class names forward; 23 E2E specs key off them. Also re-add
.task-title-placeholder on the no-task FAB so prep-state checks find it.

- .pause-resume-btn on pause/resume in focus-mode-main + focus-mode-break
- .complete-session-btn on the done_all button in focus-mode-main
- .task-title-placeholder added to .select-task-cta FAB

* fix(focus-mode): aria-label icon buttons; align break E2Es with new flow

- focus-mode-break: pause/resume/skip/reset icon buttons now carry
  [attr.aria-label] in addition to matTooltip — icon ligature alone
  isn't an accessible name and breaks getByRole locators.
- pomodoro-break-timing-bug-6044.spec: skipButton uses getByRole with
  accessible name rather than hasText (mat-icon ligature is "skip_next",
  not "skip break").
- focus-mode-break.spec: "exit break to planning and change timer mode"
  and "Back to Planning should NOT auto-start next session" now match
  the unified back-to-planning flow — overlay closes on click, user
  re-opens focus mode to change settings or verify prep state.

* fix(focus-mode): address PR #7586 review feedback

Maintainer review (johannesjo):
- Debounce the Pomodoro work-duration write so editing it emits one
  synced config op instead of one per keystroke; flush on session start
  so a value typed inside the debounce window is not lost. (A1)
- Replace the local ::ng-deep restyling of the shared input-duration-slider
  with opt-in [bareRing] and [hideHandle] inputs that own the chrome
  overrides in the slider's own styles; the four other consumers keep the
  default look. (A2)
- Add unit tests for the flexible drag math (_setValueFromRotationFlex):
  the A<->B boundary anchoring at 55/60 min and both +/-180 degree wrap
  branches. (A3)
- Delete the orphaned exitBreakToPlanning action, its
  stopTrackingOnExitBreakToPlanning$ effect, reducer case and specs;
  cancelFocusSession already unsets the current task. (B1)
- Drop the unreferenced CONTINUE_TO_NEXT_SESSION and BREAK_RELAX_MSG
  i18n keys. (B2)
- Collapse the duplicated clock-size clamp() into a single
  --clock-face-size-default token. (B4)

Smaller focus-screen fixes (beerkumquatpome):
- Hide the break task title when "pause tracking during breaks" is on. (C7)
- Commit and close the duration editor on Enter. (C9)
- Rename "Back to Planning" to "Exit focus session". (C11)
- Show Flowtime breaks as a neutral "Break". (C13)

Break-circle vertical alignment (C1) is only partially addressed here
(matched the top reservation); exact alignment is a follow-up.

* refactor(focus-mode): share a layout shell across timer screens and auto-start Flowtime breaks

Extract a presentational focus-mode-layout component (4-row content-projection
skeleton: [fmTop]/[fmTask]/[fmClock]/[fmBottom]) shared by the focus-session and
break screens, so both keep a stable clock baseline across the focus<->break and
prep<->in-progress transitions. focus-mode-main and focus-mode-break now consume
the shell instead of each maintaining their own absolute layout.

Replace the Flowtime "break offer" step with an auto-started break, mirroring
Pomodoro:
- Remove the BreakOffer UI state and the offerFlowtimeBreak action/reducer.
- endFlowtimeSession now dispatches completeFocusSession(isManual:false) +
  startBreak (unsetting the task first when tracking-pause-on-break is on), so
  the break starts automatically and the session is logged exactly once via
  logFocusSession$.

Also reorder the bottom controls (Back to Planning leftmost), restore the
BACK_TO_PLANNING label to "Back to Planning", and drop the now-orphaned
FLOWTIME_BREAK_TITLE / START_BREAK i18n keys.

* test(layout): restore document.activeElement after focus-restoration specs

The LayoutService "Focus restoration" tests override document.activeElement
with Object.defineProperty, which shadows the native (inherited) getter with an
own property on document. The afterEach only removed the mock DOM node, so the
override leaked into later specs: once it ran, document.activeElement was frozen
at the mock element and subsequent .focus() calls could no longer move it.

Depending on Karma's spec order this broke the task.service focusTaskById tests
(#7120), which then saw the stale activeElement instead of the element they
focused. Delete the shadowing own property in afterEach to restore native
behavior.

Repro: ng test --include layout.service.spec.ts --include task.service.spec.ts

* refactor(focus-mode): add interactive tracking widget and polish timer-screen layout

- Replace the read-only task-tracking-info with focus-mode-task-tracking
  (vertical time stack + play/pause), wired through the shared layout shell
- Center the task title and floor the task-row height so the clock baseline
  stays aligned across focus <-> break
- Spacing polish: task-title-row and layout gaps to --s2,
  segmented-button-group padding to 0
- Drop redundant safe-area-bottom padding on the action row (the overlay
  already reserves it for the fixed shell)
- Add "Take a moment to relax" break message and a clock-digit edit affordance

* refactor(focus-mode): share timer/break layout, drop dead tracking toggle

- Extract a shared <focus-mode-layout> skeleton and <focus-mode-task-row> used by both the focus session and the break.

- Remove the in-view tracking play/pause toggle (start/stop stays on the global header button); strip focus-mode-task-tracking to read-only and drop RESUME_TRACKING.

- Pin the mode selector out of flow and center the task·clock·bottom group; equal reserved task/bottom rows keep the clock vertically centered, with the selector kept on top.

- Tighten sizing: horizontal selector segments, settings cog matched to the in-session controls, and a fluid clock clamp.
2026-06-12 11:59:56 +02:00
..
scenarios feat(focus-mode): focus screen UX overhaul (#7586) 2026-06-12 11:59:56 +02:00
seed Improve automated store screenshots 2026-05-10 00:58:01 +02:00
build-store-assets.ts fix(build): remove sharp from deps to fix F-Droid build 2026-05-18 21:59:24 +02:00
composite-mac-chrome.ts fix(build): remove sharp from deps to fix F-Droid build 2026-05-18 21:59:24 +02:00
fixture.ts Improve automated store screenshots 2026-05-10 00:58:01 +02:00
helpers.ts Improve automated store screenshots 2026-05-10 00:58:01 +02:00
load-sharp.ts chore: remove unused eslint disables 2026-06-03 21:31:06 +02:00
marketing-copy.ts fix(restore): undo screenshot-pipeline reverts from 1d52843c 2026-05-08 22:30:59 +02:00
matrix.ts feat(screenshots): automate flathub output 2026-05-11 13:35:17 +02:00
print-output-path.ts test(screenshots): stabilize electron screenshot output 2026-05-09 14:38:49 +02:00
README.md feat(screenshots): automate flathub output 2026-05-11 13:35:17 +02:00

Automated screenshot pipeline

Reproducible app-store screenshots driven by Playwright + a single seed dataset.

Quick start

# Capture the web-store matrix (all web viewports × matching scenarios)
npm run screenshots

# Or split:
npm run screenshots:capture          # full web matrix → dist/screenshots/_master/
npm run screenshots:capture:desktop  # desktopMaster only
npm run screenshots:capture:mobile   # iPhone/iPad/Android viewports only
npm run screenshots:capture:electron # Electron build → dist/screenshots/_master_electron/
npm run screenshots:electron         # capture:electron + build (lands in dist/)
npm run screenshots:build:flathub    # rebuild only dist/screenshots/flathub/
npm run screenshots:flathub          # Linux Electron capture + Flathub-ready build
npm run screenshots:build            # rebuild dist/ layout from existing masters

# One group while iterating
npx playwright test --config e2e/playwright.store-screenshots.config.ts \
  --project=desktopMaster --grep "desktop all"

Environment overrides

Var Effect
SCREENSHOT_MODE=electron Switches the fixture to the Electron pipeline (set by screenshots:capture:electron).
SCREENSHOT_BASE_DATE=2026-05-06T09:30:00 Pin the "today" anchor used by the seed builder. Default is a Wednesday well clear of midnight in CI timezones.
SP_SCREENSHOT_BG_DARK_URL / SP_SCREENSHOT_BG_LIGHT_URL Override the default Unsplash backgrounds (e.g. point to a vendored asset for offline / privacy-sensitive runs).
SP_SCREENSHOT_BG_DISABLE=1 Drop background images entirely (set by screenshots:flathub).
SP_SCREENSHOT_BG_OVERLAY_OPACITY=80 Drives the per-context "Darken/lighten background image for better contrast" slider (099). Default 80 for screenshots vs. 20 in the app.
SP_SCREENSHOTS_STORE=flathub Restrict post-processing to one store rule. Used by screenshots:build:flathub so Linux captures do not regenerate Mac App Store output.

Master captures land in dist/screenshots/_master/<viewport>/<locale>/<theme>/<scenario>/<name>.png. Per-store assets land in dist/screenshots/<store>/<locale>/NN-name.png (and the F-Droid fastlane/... layout). Web and Microsoft Store share the generic desktop output at dist/screenshots/desktop/<locale>/.

Scenario lineup

Slot Platform Theme What it shows
mobile-00 mobile dark Cover/hero — Today list with marketing caption overlay
desktop-00 desktop dark Cover/hero — Today list with marketing caption overlay
tablet-00 tablet dark Cover/hero — Today list with marketing caption overlay
mobile-01 mobile dark Planner
mobile-02 mobile dark Planner with calendar nav expanded
mobile-03 mobile dark Eisenhower matrix board
mobile-04 mobile light Planner expanded (light variant)
mobile-05 mobile dark Schedule view
mobile-06 mobile dark Today task list
desktop-01 desktop dark Today + schedule day-panel open
desktop-02 desktop dark Eisenhower matrix board
desktop-03 desktop dark Schedule view
desktop-04 desktop light Project (Work) + notes panel populated
desktop-05 desktop dark Focus mode
desktop-06 desktop light Schedule (light variant)
desktop-07 desktop dark Project (Work) view, no wallpaper — regular palette reads cleanly
desktop-08 desktop dark Planner
desktop-09 desktop light Project (Work) + issue provider panel open
desktop-10 desktop dark Project (Work) + task detail panel open

Specs are platform-grouped: scenarios/desktop/all.spec.ts and scenarios/mobile/all.spec.ts each capture every slot in a single session, flipping DARK_MODE between groups via applyTheme() (Playwright addInitScript is append-only, so a later script wins on each reload). Each spec runs once per locale (en + de).

Files

Path Purpose
matrix.ts Locales, themes, viewports, mobile/desktop classification, store rules, shared desktop output
seed/seed.template.json Curated dataset with date offsets and @@PLANNER_OFFSET_+N placeholders
seed/build-seed.ts Materializes offsets to absolute dates, injects locale + customTheme
fixture.ts Pins clock, applies dark-mode, imports seed via UI flow, exposes screenshotMaster (which reads live DARK_MODE so light/dark scenes land in the right directory)
helpers.ts gotoAndSettle, panel open helpers, resetView, applyTheme, applyLocale, applyTimeTrackingEnabled, applySideNavCollapsed, setPlannerCalendarExpanded, showMarketingOverlay
marketing-copy.ts Headline + subline shown on the slot-00 hero overlay
scenarios/desktop/all.spec.ts 12 desktop captures: hero + 11 scenes / light variants
scenarios/mobile/all.spec.ts 7 mobile slots: hero + 6 scenes
scenarios/tablet/all.spec.ts 6 tablet slots: hero + 5 scenes
build-store-assets.ts Renames + copies masters into shared/per-store layouts; filters/frames Flathub; JPEG re-encode for maxBytes-capped stores (Snap); emits _preview.html contact sheet
../playwright.store-screenshots.config.ts Separate Playwright config; one project per viewport

How it works

  1. Per-test seed file is materialized to .tmp/screenshot-seeds/seed-<date>-<locale>[-<customTheme>].json.
  2. Fixture boots the app with page.clock.install({ time: SCREENSHOT_BASE_DATE }), sets localStorage.DARK_MODE, pins browser context to en-US (so ImportPage's English text matchers always work), then imports the seed via BackupService.importCompleteBackup. Locale flows through globalConfig.localization.lngapplyLanguageFromState$ effect. Custom themes flow through globalConfig.misc.customTheme.
  3. Each scenario spec drives the app to a state and calls screenshotMaster(scenario, name). The Playwright project name (e.g. desktopMaster) determines the viewport.
  4. Post-processor copies masters into shared/per-store layouts. No resizing — captures are already at native size for each store.

Adding a scenario

// scenarios/desktop/08-new-thing.spec.ts
import { test } from '../../fixture';
import { LOCALES } from '../../matrix';
import { gotoAndSettle, onlyOn } from '../../helpers';

for (const locale of LOCALES) {
  test.describe(`@screenshot desktop-08-new-thing (${locale})`, () => {
    test.use({ locale, theme: 'dark' });

    test('new thing', async ({ seededPage, screenshotMaster }, testInfo) => {
      onlyOn(testInfo, 'desktop');
      await gotoAndSettle(seededPage, '/#/whatever');
      await seededPage.locator('whatever-component').waitFor();
      await screenshotMaster('desktop-08-new-thing', 'new-thing');
    });
  });
}

Per-store gotchas

  • Web / MS Store share dist/screenshots/desktop/<locale>/; MS Store reserves bottom 1/4 for system-rendered captions and accepts up to 10 screenshots, so pick at most 10 from the shared desktop set before upload.
  • Mac App Store rejects letterboxed 16:9 in 16:10 frames — capture native 2880×1800.
  • Snap uses the same desktop master content but stays separate because it caps at 5 items, ≤2 MB each, single global gallery (no per-locale). Pipeline emits all desktops re-encoded as JPEG (mozjpeg, q90→q60 step-down) so each fits the cap; trim manually to 5 before Snap upload.
  • Play / Apple explicitly forbid / discourage device frames.
  • Apple requires only iPhone 6.9" (1290×2796) and iPad 13" (2064×2752); smaller sizes auto-derive.
  • Flathub requires native window chrome and forbids overlays — sourced from the Electron capture pipeline (single global gallery). Run on a Linux X11/Wayland host via npm run screenshots:flathub; it disables decorative backgrounds, drops the marketing hero/duplicate variants, and frames the final PNGs with transparent rounded corners + shadow.

Electron pipeline (Mac App Store, Flathub)

Web Chromium captures don't look "native" on macOS — wrong fonts, wrong scrollbars, no traffic-lights. Flathub explicitly requires native window chrome. So there's a parallel pipeline that runs the actual SP Electron build via Playwright's _electron API. macOS captures use Electron renderer screenshots plus a deterministic hiddenInset traffic-light overlay; Linux captures use OS-level region tools (grim/import) so Flathub gets real GTK chrome.

# Capture only — masters land in dist/screenshots/_master_electron/.
npm run screenshots:capture:electron

# Capture + build — masters and deliverables under dist/screenshots/
# (macappstore/, flathub/). Mirrors `npm run screenshots` for the web pipeline.
npm run screenshots:electron

# Flathub-ready Linux capture + targeted build. Disables decorative backgrounds
# and emits the filtered/framed dist/screenshots/flathub/ gallery.
npm run screenshots:flathub

Same scenarios, same fixture file — store-screenshots/fixture.ts branches on the SCREENSHOT_MODE env var (the npm script sets it). Each desktop spec runs unchanged in either mode.

On macOS, Playwright-launched Electron is not always treated like a LaunchServices-started .app, and OS capture can miss AppKit's hiddenInset traffic lights even when the window content is correct. The fixture avoids that fragile path: it captures the renderer at the target 2560×1600 Retina size and composites the three traffic lights at AppKit's hiddenInset coordinates.

On Linux, the OS-level capture grabs the full window rect including titlebar, shadow, and GTK decoration. Bounds come from BrowserWindow.getBounds(); on X11/Wayland bounds == pixels.

Per-OS tooling (must be on PATH):

  • macOS — no external capture tool; the fixture forces Retina-scale capture so the renderer screenshot lands at 2560×1600.
  • Linux X11 — ImageMagick (apt install imagemagick, ships import)
  • Linux Waylandgrim (apt install grim, wlroots-based compositors only)

The Mac App Store and Flathub store rules have masterDir: 'electron' in STORE_RULES, so the post-processor pulls those captures into dist/screenshots/macappstore/ and dist/screenshots/flathub/. Flathub additionally pins its gallery order and applies rounded transparent window framing in build-store-assets.ts. All other stores still come from the web pipeline.

Status

  • Foundation: matrix, seed builder, fixture (web + electron modes), helpers, post-processor
  • 26 captures covering planner, boards, schedule, focus, notes, project view, task details, and issue provider setup
  • Per-build _preview.html contact sheet under dist/screenshots/ for one-click QA
  • Electron-mode pipeline with macOS traffic-light compositing and Linux OS chrome capture (grim / import)
  • Mac App Store wired to source from _master_electron/
  • Flathub STORE_RULE (single-gallery, sourced from _master_electron/, filtered/framed for Flathub)
  • Snap JPEG re-encode under 2 MB cap (mozjpeg, automatic per-file)
  • Tooltip suppression + cursor parking so leftover Material tooltips don't bleed into captures
  • Smoke-test the Electron pipeline on a real Mac (or Linux X11 for plumbing)