Skip to content

# ISP-077: Map style / contrast switch vs canvas-worker re-raster vs Playwright layout-snapshot pixel churn breaking every CI run #747

Closed

Description

@Mikey-222
  • Type: Integration + Spike
  • Severity: P0/Critical
  • Difficulty: Extreme
  • Estimate: 24h
  • Touch surface: 13 files

1. Background & Context

HelPhone's map must switch styles (streets / satellite / high-contrast emergency) instantly: src/components/CommunityMap.jsx calls map.setStyle(), src/components/MapboxWrapper.jsx / .tsx (mapbox-gl) reloads glyphs/sprites/tiles, src/components/WebGPUMap.jsx (+ .d.ts) re-rasters its overlay via src/workers/canvas-worker.js through src/lib/offscreenCanvas.ts + src/lib/worker-manager.js + src/lib/workerSandbox.ts, and src/App.css keyframes (mdpulse, mddash, mdfloat) + component layout CSS repaint the surrounding cards. Contrast switching is an accessibility P0 (low-vision volunteers, sunlight glare) wired through src/stores/helpStore.ts preference + src/i18n.ts locale direction (RTL flips map controls). The intended flow in docs/performance-optimization.md is style-swap with cached raster, sub-500ms, no layout shift.

Reality is re-raster + relayout + pixel churn. map.setStyle() in mapbox-gl is effectively a full reload: every source re-adds, useClusterer.js (supercluster via clusterWorker.js / cluster-worker.js) reindexes, src/lib/geocoder.ts (fuse.js) re-queries visible labels, and canvas-worker.js discards its bitmap cache and re-rasters all pins at the new pixelRatio/contrast palette — 600–1200ms of blank + flash on desktop, 2–4s on low-end Android. WebGPUMap.jsx and Mapbox race: Mapbox finishes tiles first and presents low-contrast pins, WebGPU overwrites 400ms later with high-contrast, producing a double-flash that Playwright screenshots catch mid-transition non-deterministically.

CI then fails on purpose. Layout snapshots (Playwright toHaveScreenshot / Percy-style) compare map + cards pixel-for-pixel, but map tiles are live (traffic, labels), mddash route animations + mdpulse responder rings never settle, hero video (assets/hero-nokia.mp4 poster in index.html + resourceHints.ts) paints different frames, and font/i18n rendering (ISP-079 CJK/Arabic) anti-aliases differently per runner. Every style-switch PR greens locally (cached tiles, settled animations) and reds in CI (fresh tiles, mid-pulse capture). Teams respond by raising threshold to 8% or deleting snapshots, so real contrast regressions (pins invisible on satellite) ship uncaught — the exact harm accessibility switching was meant to prevent.

The blast radius spans workers and config. routingWorker.js + src/utils/graphTraversal.js route colors must also re-contrast (not just pins), src/lib/audioAlert.ts contrast-beep mapping drifts, and vite.config.ts manualChunks sprite/style JSON chunking changes style-load timing per build. src/components/ErrorBoundary.tsx + src/lib/crashRecovery.ts treat style-load 404s (stale precache sprite, ISP-072) as crashes, offering reload instead of style fallback.

2. Current Broken Behaviour

  • Contrast switch blanks map 600ms–4s (style reload + cluster reindex + canvas re-raster serial); volunteers lose situational awareness mid-toggle.
  • Double-flash: Mapbox low-contrast pins visible 300–500ms before WebGPU high-contrast overwrite; photosensitive flicker risk + screenshot flake.
  • Playwright layout snapshots flake 30–60% on map + style tests: tile liveliness + mdpulse/mddash animations + video frame + font AA guarantee pixel diff every run.
  • Threshold raised to silence flakes now masks real bugs: white-on-white pins on satellite shipped twice, caught only by manual a11y audit.
  • Route colors (graphTraversal.js palettes) do not re-contrast with pins; after switch, routes invisible while pins correct (half-switched map).
  • Stale-precache style sprite 404 → ErrorBoundary full-page crash instead of graceful streets-fallback; field users on old SW cannot switch styles at all.

3. Desired End State

  • Instant perceived switch: optimistic pin/route recolor (<100ms, CSS/canvas palette swap) while setStyle + re-raster settle in background; skeleton/shimmer, never blank; WebGPU + Mapbox present same tick (ISP-071 frame truth reused).
  • Deterministic visual tests: map snapshots use pinned style fixture + frozen tiles + disabled animations (prefers-reduced-motion + CSS animation off + mddash paused) + font stabilization; pixel tests assert pin/route contrast tokens, not live-tile bytes; live-tile liveliness masked.
  • Token-driven contrast: single contrast-token source consumed by Mapbox style, canvas-worker.js raster, routingWorker.js route palette, and card CSS; switch flips tokens, all layers agree, half-switched impossible.
  • Graceful style failure: sprite/style 404 falls back to cached streets + banner (not crash); telemetry.ts logs map.style_fallback with style id + contrast mode.
  • CI green + sensitive: snapshot threshold ≤1.5% on masked map, real contrast regressions (Δ luminance below WCAG) fail deterministically.

4. Files / Systems Involved (10-15 real paths)

  1. src/components/CommunityMap.jsx — setStyle orchestration to rework
  2. src/components/MapboxWrapper.jsx + src/components/MapboxWrapper.tsx — style lifecycle twins
  3. src/components/WebGPUMap.jsx + src/components/WebGPUMap.d.ts — overlay re-raster path
  4. src/workers/canvas-worker.js + src/lib/offscreenCanvas.ts — bitmap cache invalidation point
  5. src/workers/clusterWorker.js + src/workers/cluster-worker.js + src/hooks/useClusterer.js — reindex-on-style waste
  6. src/workers/routingWorker.js + src/utils/graphTraversal.js — route palette half-switch
  7. src/stores/helpStore.ts — contrast preference persistence
  8. src/App.css + component layout CSS — animation pause + token plumbing
  9. src/lib/resourceHints.ts + index.html + assets/hero-poster.png — hero determinism for snapshots
  10. vite.config.ts — style/sprite chunk timing
  11. src/lib/telemetry.ts — style_switch_ms + style_fallback metrics
  12. src/components/ErrorBoundary.tsx + src/lib/crashRecovery.ts — 404-as-crash misroute
  13. docs/performance-optimization.md — style-switch budget to enforce

5. Reproduction Steps (numbered, with real commands)

  1. grep -n "setStyle\|style.*contrast\|high-contrast\|palette\|token" src/components/CommunityMap.jsx src/components/MapboxWrapper.jsx src/components/MapboxWrapper.tsx src/components/WebGPUMap.jsx src/utils/graphTraversal.js src/App.css | head -40 — map style/contrast plumbing.
  2. grep -n "toHaveScreenshot\|toMatchSnapshot\|mask\|animations.*disabled\|prefers-reduced" tests/*.spec.ts playwright.config.* 2>/dev/null | head -30; ls tests/ | head — snapshot setup audit.
  3. npm run dev -- --port 3000 & sleep 5 — open map, toggle contrast 5×, screen-record; time blank duration + count flashes.
  4. npx playwright test tests/map-style.spec.ts --repeat-each=10 2>&1 | grep -E "passed|failed|flaky|snapshot" | head -20 — quantify flake rate; npx playwright test --update-snapshots locally then rerun in CI to show churn.
  5. Freeze probe: npx playwright test tests/map-style.spec.ts --project=chromium --reporter=line 2>&1 | tee /tmp/style.log; grep -n "pulse\|dash\|tile\|font" /tmp/style.log | head — attribute diff sources (open test-results/*/actual.png vs expected.png diff).
  6. Half-switch: toggle contrast, screenshot routes vs pins; grep -n "color\|palette" src/workers/routingWorker.js src/utils/graphTraversal.js | head — prove route palette stale.
  7. Sprite-404: npm run preview -- --port 4173 with SW-seeded old sprite URL blocked (npx playwright route --block="*sprite*" or DevTools block), toggle style; confirm full-page ErrorBoundary instead of fallback.
  8. cat docs/performance-optimization.md | grep -n -i "style\|contrast\|raster" | head — budget vs blank reality.

6. Spike Investigation Plan

  1. Switch profiling: instrument setStyle → tiles-loaded → cluster-ready → raster-present waterfall via telemetry.ts; identify serial waits (reindex blocks raster) and blank window per device tier.
  2. Optimistic recolor: prototype token-swap (CSS variables + canvas palette LUT) applied synchronously to pins/routes/cards before setStyle resolves; measure perceived-switch ms vs full-settle ms; verify no double-flash with ISP-071 tick gate.
  3. Cache-preserving style diff: test mapbox setStyle({diff:true}) / source-preserving swap vs full reload; keep supercluster index + bitmap cache across styles where projection unchanged; quantify reindex/raster savings.
  4. Deterministic harness: build snapshot fixture (pinned style JSON, local tile stub, animations animation: none !important, frozen video poster, font preload await); mask live-tile region; assert 20 repeat runs zero-diff on unchanged code.
  5. Token single-source: extract contrast tokens (pin fill/stroke, route colors, card bg/fg, focus rings) to shared module consumed by Mapbox style fn, canvas-worker.js, routingWorker.js, CSS; add WCAG luminance assertion per token pair.
  6. Failure fallback: style/sprite 404 → cached streets + banner + style_fallback span; remove ErrorBoundary misroute; test under ISP-072 stale-precache.
  7. CI policy: threshold ≤1.5% masked, live-tile tests separate from token-contrast unit snapshots; document when to update snapshots (token change) vs when flake must be fixed (infra).

7. Acceptance Criteria (8-12 hard checkboxes)

  • Perceived contrast switch <150ms (optimistic recolor), full settle <1.2s desktop / <2.5s low-end (traced, not felt)
  • Zero blank map during switch (skeleton/palette-swap, never grey); zero double-flash (single coordinated present)
  • Route + pin + card contrast switch atomically (screenshot shows no half-switched state across 20 toggles)
  • Snapshot suite 20/20 green locally + CI with threshold ≤1.5% (masked live tiles, frozen animations/poster/fonts)
  • Real regression caught: white-on-satellite fixture fails deterministically (mutation test proves sensitivity, not threshold-silenced)
  • Sprite/style 404 falls back to streets + banner (no full-page crash); style_fallback span emitted
  • WCAG AA contrast asserted per token pair in unit test (pins, routes, focus rings) across all styles
  • telemetry.ts emits style_switch_ms (perceived + settle) + raster_present_ms; dashboard + alert on p95 breach
  • Snapshot update policy documented; intentional token change updates snapshots once, infra flake never updates snapshots
  • RTL + CJK snapshots stable (ISP-079 fonts pinned) — no per-runner AA churn

8. Why This Takes 24h / Why It Is Extremely Hard

  • Style switch touches GPU (tiles), CPU (reindex), worker (raster), and CSS (cards) simultaneously; profiling the serial chain and proving optimistic recolor without double-flash needs coordinated traces across all four.
  • Deterministic map snapshots fight live tiles + never-settling CSS/SMIL animations + video frames + cross-runner font AA; freezing each without masking real regressions is a full-day fixture build with 20× repeat validation.
  • Token single-sourcing spans Mapbox style spec, canvas LUT, worker palettes, and CSS variables — four color systems with different gamuts; WCAG-proofing each pair per style is combinatorial.
  • Cache-preserving setStyle diff behavior is version-sensitive in mapbox-gl; correct diff vs reload choice needs version-pinned experiments, not docs.
  • CI determinism (fonts, GPU-less tile rendering, threshold calibration) requires runner-level control (docker image pins, font installs) beyond app code, each iteration a slow CI cycle.

9. Risks & Blast Radius

  • Optimistic recolor can lie (shows high-contrast pins over stale low-contrast tiles) and mask tile-load failures; needs settle-timeout + fallback so optimism never strands.
  • Freezing animations for tests (animation: none) can leak into prod via shared CSS; fixture scoping must be test-only or a11y reduced-motion users lose cues.
  • Token refactor recolors every map-adjacent surface at once; one wrong token ships fleet-wide illegibility — staged per-style rollout + a11y audit required.
  • Masking live tiles in snapshots blinds tile-outage detection; separate liveness probe (non-pixel) must cover what snapshots mask.
  • Style-fallback to streets overrides user emergency preference (dark/high-contrast at night); fallback must preserve contrast mode, not just style id.

Activity

  1. UltimateUser1 commented on Oct 18, 2026

    @UltimateUser1
    Contributor

    @UltimateUser1 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    Hey, I'd love to take this on! I'm a full-stack blockchain dev with experience in Next.js, TypeScript, Tailwind, smart contracts, blockchain integrations

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @UltimateUser1 to this issue.

  2. drips-wave commented on Oct 18, 2026

    @drips-wave

    Congratulations, @UltimateUser1! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.

    🧑‍💻 @UltimateUser1: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.

    Warning

    When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.

    🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.

    🌊 Happy Wave 🌊

  3. UltimateUser1 commented on Oct 18, 2026

    @UltimateUser1
    Contributor

    Addressed in #839: layout snapshots freeze CSS animations and mask live map canvases.

  4. drips-wave commented on Oct 18, 2026

    @drips-wave

    This issue has been completed by @UltimateUser1 as part of the Stellar Wave Program's 10th Wave 🥳

    😎 @UltimateUser1: You earned 200 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.

    🧑‍💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions