Masking Map Attribution and Scale Bars
Attribution, scale bars, zoom indicators and provider logos are the first things a team masks, usually all at once and usually on the first day. Two of the four do not need it: a scale bar and a zoom indicator are pure functions of the camera, so pinning the camera makes them deterministic without removing anything from the comparison. The other two vary for reasons genuinely outside the application, and even there masking is the third-best option. This page separates them and gives the cheaper treatment for each.
This is a task within Interactive Overlay Masking Rules, under Dynamic Element Masking & UI Stability. It assumes the camera fixture from Viewport & Zoom Sync Strategies is already in place.
Prerequisites
Which of the four actually vary
The grouping is worth internalising because it generalises: chrome that derives from state the test already controls needs no special handling, and chrome that carries content from elsewhere needs the content controlled rather than the pixels hidden. Almost every element on a map falls into one of those two categories, and reaching for a mask is a sign that neither question was asked.
Attribution: serve the text, do not hide it
The style’s attribution is usually declared per source, which means it can be pinned in the same fixture that pins the style:
{
"sources": {
"basemap": {
"type": "vector",
"tiles": ["http://127.0.0.1:8080/tiles/{z}/{x}/{y}.pbf"],
"attribution": "© Fixture Data 2026"
}
}
}
With a fixed string, the whole attribution element stays under test: its position in the corner, its font and size, whether it wraps at a narrow viewport, whether it collides with the scale bar, and whether a long string overflows its container. Those are real layout regressions, they occur in a corner of the frame where nobody looks, and a mask removes every one of them.
If the attribution is injected by a third-party control the test cannot reach, a fixed-width placeholder substituted through an init script is the next-best option, and a mask is the last.
Scale bar and zoom indicator: pin the inputs
Two practical notes follow from the diagram.
Pin the locale, not just the camera. A scale bar rendered in an environment defaulting to imperial units shows 500 ft where the baseline shows 200 m, and the difference has nothing to do with the map. Set locale on the browser context, and pin it in the runner image as well, so a container-level default cannot override it.
Watch the rounding boundary. Scale bars pick a “nice” number — 100, 200, 500 — from the available pixel width, and a camera that differs in the sixth decimal place can land on the other side of a boundary and flip the label. This is the one case where the scale bar is genuinely unstable, and the fix is upstream: the camera precision discipline in Serializing and Restoring Map Camera State for Tests removes it entirely. Masking the scale bar to avoid it would hide the symptom of a camera that is not actually being restored exactly, which is a much larger problem than a label.
A checklist for any piece of chrome
The two questions that sorted the four elements above generalise into a short routine, and running it takes about a minute per element.
Does this derive from state the test already controls? Camera, viewport, locale, timezone, theme and the frozen clock are all pinned by the harness. Anything computed from them — a scale bar, a coordinate readout, a zoom badge, a north arrow reflecting bearing, a formatted timestamp — is already deterministic and needs nothing. If it is not stable in practice, that is a report about the pin rather than about the element, and the pin is where the fix belongs.
Does it carry content from outside the application? Attribution strings, provider logos, third-party widget contents and anything fetched from a service the team does not own. For these, control the content: a fixture string, a checked-in asset, a stubbed response. The pixels stay under test and only the words are fixed.
Is it genuinely unreachable? Only after both questions have been answered no does a mask become the right answer, and then it gets a manifest entry with a reason, an owner and a review date — because “unreachable” is often a statement about how the application is currently wired rather than a permanent fact, and the review date is what prompts someone to check.
The routine matters because masking is self-reinforcing: each mask makes the suite greener, which makes the next mask easier to justify, and no individual decision looks unreasonable. Tracking the masked share of the frame as a number, as described in Dynamic Element Masking & UI Stability, is what makes the accumulated cost visible before it is large.
Verification
Confirm the procedure worked before wiring it into a blocking gate:
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The scale bar label flips between two values on repeat runs | The camera is being restored approximately, and the underlying pixel length sits on a nice-number boundary | Fix the camera restoration precision rather than masking the bar; the flip is a symptom worth keeping |
| Attribution differs only in CI | The container’s locale or font differs, changing the wrapping of the same string | Pin locale and fonts in the runner image; the string itself was never the problem |
| The attribution mask covers part of the map | The element grows when the string is long, and the mask was sized against a short one | Serve a fixed string instead, or derive the mask box at capture time rather than storing a rectangle |
Frequently asked questions
Is it acceptable to hide attribution entirely during tests?
Removing it changes the layout — other corner controls reposition — so the capture no longer represents the product. It is also worth being careful about: attribution is frequently a licence requirement of the data provider, and a habit of switching it off in test builds has a way of reaching a staging environment that someone external then sees. Serving a fixture string keeps both the layout and the habit correct.
What about a zoom indicator that shows fractional zoom?
It becomes deterministic exactly when the camera does, and it is a useful canary: a zoom indicator reading 14.02 where the fixture said 14 is a visible, human-readable report that the camera restoration is not exact. That makes it one of the more valuable pieces of chrome to keep in the comparison rather than mask.
Should chrome have its own tolerance class?
Yes — it is DOM-rendered rather than rasterised by the map, so its noise floor is much lower than anything on the canvas. Giving it a tight class catches layout regressions that a canvas-calibrated tolerance absorbs entirely, and it costs one entry in the region map. The classes are described in Per-Region Tolerance Classes for Map Interfaces.
Does this change on a retina capture?
The scale bar does: at a different device pixel ratio the same ground distance covers a different number of CSS pixels only if the viewport changes, but the bar’s rasterisation changes and the nice-number choice can differ if the control measures in device pixels. It is one more reason to treat DPR as a deliberate axis rather than something that varies with the runner.
Related
- Up to Interactive Overlay Masking Rules, and the section Dynamic Element Masking & UI Stability.
- Serializing and Restoring Map Camera State for Tests — the precision that makes the scale bar stable.
- Masking CSS Selector Regions for Map Popups — the mask mechanics for what genuinely needs one.
- Per-Region Tolerance Classes for Map Interfaces — giving chrome its own tight tolerance.