DEV Community

Cover image for The map was blank. Every test was green.
Onkar Deokate
Onkar Deokate

Posted on

The map was blank. Every test was green.

The driver map: blank, then every parking spot piled into one corner, then every spot in place.

The most important screen in a parking app is the map. It's the first thing a driver sees, and the only reason they opened the app.

On the web build of ParkEase, that map was blank.

No crash. No red error box. No failing test. Just an empty rectangle where Bangalore should have been, sitting quietly above a perfectly working list of parking spots. Every test passed, because no test looked at the map.

Yesterday I promised you the story. Here it is: three bugs, stacked so neatly that fixing one only revealed the next.

Bug one: the worker that was a web page

MapLibre, the open-source map renderer ParkEase uses, does its heavy lifting in a Web Worker. To find that worker's file, MapLibre 6 builds a URL relative to its own module.

Under Expo's web bundler there is no "own module". There's one big bundle. So the URL pointed at a file that didn't exist, and the dev server did what dev servers do with unknown paths: it served the app's HTML page.

MapLibre asked for JavaScript, got HTML, and the worker died before processing a single tile. Nothing on screen said so.

The fix is to tell MapLibre exactly where its worker lives, and to make sure it's really there:

export const MAPLIBRE_WORKER_URL = '/maplibre/maplibre-gl-worker.mjs';
Enter fullscreen mode Exit fullscreen mode
try {
// Before any Map is created: MapLibre spawns its workers on the first one.
(candidate as WebMapModule).setWorkerUrl(MAPLIBRE_WORKER_URL);
Enter fullscreen mode Exit fullscreen mode

A postinstall script copies the worker, and every chunk it imports, into the folder the web build serves. It follows the worker's own imports instead of a hard-coded file list, so a future MapLibre release that splits out another chunk gets copied too. If the copy fails, the install fails, loudly, and names the web map. A silent copy failure is how this whole story started.

Bug two: the box with no height

Workers fixed. Tiles loading. Map... still blank.

The CSS ParkMap injects (the app doesn't load MapLibre's own stylesheet) had one extra rule: overflow: hidden on the canvas container. That container only holds absolutely positioned children, so it has zero height. Clip a zero-height box and you clip everything inside it: the canvas and every marker.

The fix was deleting a line. The test that keeps it deleted:

it('never clips the map canvas container', () => {
  for (const body of rules(MAPLIBRE_CSS, '.maplibregl-canvas-container')) {
    expect(body).not.toMatch(/overflow\s*:\s*hidden/);
  }
});
Enter fullscreen mode Exit fullscreen mode

Bug three: every spot in one corner

This is my favourite, because the map came back. Streets, parks, the blue dot where you're standing. And in the top-left corner, one lonely price tag: ₹20.

I counted the marker elements: twenty, all with the same coordinates.

Here's why. MapLibre places each marker by writing a transform onto the marker element, something like translate(412px, 318px). Our entrance animation, a gentle scale-and-rise as spots appear, also animated transform. A CSS animation beats an inline style, so every marker's position was overwritten with scale(1) translateY(0). Which, as positions go, is the top-left corner.

The fix keeps the exact same animation and moves it onto the individual scale and translate properties. Those compose with transform instead of replacing it:

`@keyframes parkease-marker-in{from{opacity:0;scale:0.6;translate:0 6px}to{opacity:1;scale:1;translate:0 0}}`,
Enter fullscreen mode Exit fullscreen mode

And the test that will catch the next person (probably me) who reaches for transform:

it('never animates transform on the marker element MapLibre positions', () => {
  expect(keyframes(MAPLIBRE_CSS, 'parkease-marker-in')).not.toMatch(/(^|[{;])\s*transform\s*:/);
});
Enter fullscreen mode Exit fullscreen mode

Ten tests now guard the map: three on the CSS, four on how the map starts (including that it shows a real "map unavailable" state with the reason, instead of a blank box, when the worker can't be set up), and three on the worker copy.

A note on the video: the "before" frames were reproduced on the fixed build by putting the old behaviour back: the worker request answered with the HTML page, and the old CSS rules re-injected. The broken states are real; they're just recreated rather than recorded at the time.

What I'd tell myself before starting

A test suite proves the things you thought to check. A blank screen isn't an error, so nothing failed. Now the rule is simple: if a screen can be empty, the empty state has to say why.


Tomorrow: you've seen the map break. Tomorrow you see what it's for. Open the app as a driver, find a free spot three streets away, see exactly what it costs and why, and walk up to the gate with a QR code in your hand. Five steps. Thirty seconds. Follow the series so you don't miss it.

ParkEase is a peer-to-peer parking marketplace for India, built solo and launching soon on Android.
Code: https://github.com/Deonkar/parkease · Map: © OpenStreetMap contributors · Music: ende.app (CC BY 4.0)

Top comments (1)

Collapse
 
launchgatecheck profile image
Launch Gate •

The CSS guards pin the causes you found. I'd add one browser-level check of the result too: seed two spots far apart, wait for the entrance animation to finish, then verify their marker boxes are distinct and inside the map. Repeat after a pan/zoom and a container resize. That would catch a future positioning regression even if it comes from a different CSS rule.

For the worker, testing the built static output seems important alongside the dev server: one deliberately missing imported chunk should reach the visible "map unavailable" state, rather than leaving the app shell and parking list looking healthy around an empty map.