Repository navigation
# ISP-077: Map style / contrast switch vs canvas-worker re-raster vs Playwright layout-snapshot pixel churn breaking every CI run #747
Description
- 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/mddashanimations + 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.jspalettes) do not re-contrast with pins; after switch, routes invisible while pins correct (half-switched map). - Stale-precache style sprite 404 →
ErrorBoundaryfull-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 +mddashpaused) + 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.jsraster,routingWorker.jsroute 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.tslogsmap.style_fallbackwith 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)
src/components/CommunityMap.jsx—setStyleorchestration to reworksrc/components/MapboxWrapper.jsx+src/components/MapboxWrapper.tsx— style lifecycle twinssrc/components/WebGPUMap.jsx+src/components/WebGPUMap.d.ts— overlay re-raster pathsrc/workers/canvas-worker.js+src/lib/offscreenCanvas.ts— bitmap cache invalidation pointsrc/workers/clusterWorker.js+src/workers/cluster-worker.js+src/hooks/useClusterer.js— reindex-on-style wastesrc/workers/routingWorker.js+src/utils/graphTraversal.js— route palette half-switchsrc/stores/helpStore.ts— contrast preference persistencesrc/App.css+ component layout CSS — animation pause + token plumbingsrc/lib/resourceHints.ts+index.html+assets/hero-poster.png— hero determinism for snapshotsvite.config.ts— style/sprite chunk timingsrc/lib/telemetry.ts—style_switch_ms+style_fallbackmetricssrc/components/ErrorBoundary.tsx+src/lib/crashRecovery.ts— 404-as-crash misroutedocs/performance-optimization.md— style-switch budget to enforce
5. Reproduction Steps (numbered, with real commands)
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.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.npm run dev -- --port 3000 & sleep 5— open map, toggle contrast 5×, screen-record; time blank duration + count flashes.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-snapshotslocally then rerun in CI to show churn.- 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 (opentest-results/*/actual.pngvsexpected.pngdiff). - 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. - Sprite-404:
npm run preview -- --port 4173with SW-seeded old sprite URL blocked (npx playwright route --block="*sprite*"or DevTools block), toggle style; confirm full-page ErrorBoundary instead of fallback. cat docs/performance-optimization.md | grep -n -i "style\|contrast\|raster" | head— budget vs blank reality.
6. Spike Investigation Plan
- Switch profiling: instrument
setStyle→ tiles-loaded → cluster-ready → raster-present waterfall viatelemetry.ts; identify serial waits (reindex blocks raster) and blank window per device tier. - Optimistic recolor: prototype token-swap (CSS variables + canvas palette LUT) applied synchronously to pins/routes/cards before
setStyleresolves; measure perceived-switch ms vs full-settle ms; verify no double-flash with ISP-071 tick gate. - 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. - 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. - 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. - Failure fallback: style/sprite 404 → cached streets + banner +
style_fallbackspan; remove ErrorBoundary misroute; test under ISP-072 stale-precache. - 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_fallbackspan emitted - WCAG AA contrast asserted per token pair in unit test (pins, routes, focus rings) across all styles
-
telemetry.tsemitsstyle_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
setStylediff 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 a11yreduced-motionusers 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
UltimateUser1 commentedon Oct 18, 2026
@UltimateUser1 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @UltimateUser1 to this issue.
drips-wave commentedon Oct 18, 2026
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 🌊
UltimateUser1 commentedon Oct 18, 2026
Addressed in #839: layout snapshots freeze CSS animations and mask live map canvases.
drips-wave commentedon Oct 18, 2026
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.