Masking Live Traffic Incident Markers

An incident marker looks like a single element and behaves like five. The icon and the position come from the incident record and are stable. The reported time, the elapsed duration and any severity that escalates with age are all functions of the clock. The queue-position badge is a function of how many other incidents happen to be open. Reaching for a mask over the whole callout makes all five go away at once, including the two the test was written to check. This procedure separates them and masks only what genuinely cannot be made deterministic.

This is a task within Live Data Overlay Stabilization, under Dynamic Element Masking & UI Stability. Any mask it does end up justifying belongs in the manifest described in Interactive Overlay Masking Rules.

Prerequisites

Step-by-step procedure

1. Classify every property the marker renders

Walk the marker’s template and put each rendered value into one of three buckets: stable, clock-derived, or window-derived. This takes ten minutes and determines everything that follows.

Which incident marker properties are safe to render and which must be neutralised Incident markers carry five properties and only two of them are stable enough to capture. The category and the geometry are stable, because they come from the incident record and do not change while it is open. The reported time, the elapsed-duration label and the severity escalation are all functions of the clock and change between runs unless the clock is frozen and the record timestamps are rewritten. The queue position badge is a function of how many other incidents are open, which varies with the feed window. A note says the first two are captured normally, the middle three are handled by rewriting timestamps, and the last is handled by fixing the feed window. Two properties are stable; the rest are functions of time or of the window category icon · geometry stable — capture normally reported time label rewrite timestamps against the epoch elapsed duration · severity escalation both derived from the clock queue position badge fix the feed window, not the badge masking the badge hides a real assertion; fixing the window keeps it

2. Rewrite record timestamps rather than masking their labels

A reported-time label, an elapsed-duration string and an age-escalating severity colour are all deterministic once the record’s timestamp sits at a fixed offset from the frozen epoch.

const OFFSETS_MIN = [3, 12, 40, 95];        // straddles the escalation thresholds
incidents.forEach((incident, i) => {
  incident.reportedAt = EPOCH - OFFSETS_MIN[i % OFFSETS_MIN.length] * 60_000;
});

Choosing offsets that cross the escalation boundaries is what makes the capture exercise the severity styling instead of accidentally testing one branch of it.

A mask sized to the icon against a mask sized to the whole callout The same incident marker is masked two ways. A tight mask covers only the volatile duration text inside the callout, so the icon, the callout geometry, the leader line and the anchor position all remain in the comparison. A loose mask covers the entire callout and a margin around it, which also removes the icon, the callout size and the anchor from the comparison — three assertions the test was written to make. A caption gives the sizing rule: mask the smallest rectangle that covers the volatile content, and record in the manifest why nothing narrower was possible. Mask the text, not the callout tight loose icon, geometry, leader and anchor stay under test three assertions removed along with the text record in the manifest why nothing narrower was possible

3. Fix the window so the queue badge is deterministic

A badge showing “3 of 17 open” varies with the feed window, not with the clock. The fix is at the fixture rather than at the marker: a fixed set of incidents produces a fixed count, and the badge becomes an assertion rather than noise.

4. Size any remaining mask to the volatile text alone

If a value genuinely cannot be made deterministic — a third-party estimated-clearance time, say — mask the text and nothing else.

await page.screenshot({
  mask: [page.locator('.incident-callout .clearance-estimate')],
  maskColor: '#ff00ff',
});

A garish, fixed mask colour is deliberate: it is instantly recognisable in a review, and a fixed colour cannot itself introduce a diff.

Deciding between rewriting a timestamp and masking the text it produces A decision path for each volatile label on an incident marker. The first question is whether the value derives from a clock the test controls; if it does, rewriting the record timestamp against the frozen epoch makes the label deterministic and keeps it under test. If it does not — because the value comes from a third-party service or from a source the test cannot reach — the second question is whether the label has fixed geometry; if it does, substituting a fixed placeholder string of the same length keeps the layout under test. Only when neither holds is masking the correct answer, and the manifest records that with an owner and a review date. Masking is the third answer, not the first derives from a clock the test controls? has fixed geometry? same length every time mask, with a reason rewrite the timestamp substitute a placeholder the first two keep the label’s layout under test; the third does not

5. Prefer a placeholder over a mask where the geometry is fixed

If the volatile string always has the same shape — a duration like 14 min, a distance like 2.3 km — substituting a fixed value of the same form keeps the label’s layout, wrapping and collision behaviour under test while removing the variance.

await context.addInitScript(() => {
  const original = window.formatClearance;
  window.formatClearance = () => '14 min';
});

6. Assert the incident count before capturing

As with any data-driven layer, the count is the cheap guard that catches a dropped record, an over-eager expiry and a parse failure, none of which a pixel tolerance will separate from noise.

7. Record every surviving mask with a reason and an expiry

A mask added here is a permanent blind spot until someone removes it. The manifest entry names what is masked, why nothing narrower worked, who owns it, and when it should be revisited — which is what makes the quarterly audit able to ask whether the third-party field is still third-party.

8. Keep the marker template honest about what it derives

The classification in step 1 is only reliable if the marker’s template makes its derivations visible. Two habits keep it that way, and both are worth adopting in the application rather than in the test.

Compute derived values in one place and pass them in as props, rather than calling Date.now() inside the render path. A template that reads the clock in four places has four independent things to freeze and no obvious list of them; a template that receives ageMinutes as a prop has one. The refactor is small and it makes the marker unit-testable as well as capturable.

Give every volatile field its own class name. This sounds trivial and it is what decides whether a mask can be narrow. A callout whose duration, clearance estimate and queue badge all render inside anonymous spans forces any mask to cover the whole callout, because there is nothing narrower to select. Adding three class names converts a mask that removes the icon and the geometry into one that removes twenty pixels of text.

Both changes tend to be resisted as “changes to production code for the benefit of tests”, which they are, and they are also both improvements on their own terms: fewer clock reads in a render path is better code, and semantically named fields are better markup. The visual suite is simply the thing that made the cost of not having them visible.

Verification

Confirm the procedure worked before wiring it into a blocking gate:

Troubleshooting

Symptom Likely cause Fix
Every incident renders at the same severity The rewritten offsets all fall inside one escalation band, so the styling has only one branch exercised Choose offsets that deliberately cross each threshold, as in step 2
The mask covers the icon as well as the text The selector matched the callout container rather than the volatile field Target the field’s own class; if it has none, add one — it is a smaller change than the coverage the wide mask costs
The queue badge still varies between runs The badge counts incidents in the feed, not in view, so trimming the fixture to the camera did not fix it Fix the whole incident set rather than the visible subset, and assert the total the badge reports

Frequently asked questions

Why not mask the whole callout and assert the marker position separately?

Because the separate assertion rarely gets written, and when it does it checks a projected coordinate rather than what the user sees. The callout’s rendered geometry — its size, its leader line, whether it flipped to avoid the viewport edge — is genuinely visual, and it is exactly the kind of thing that breaks silently. Keeping it in the comparison costs nothing once the text inside it is deterministic.

What mask colour should be used?

Something that cannot occur in the map: a saturated magenta is the conventional choice. The two properties that matter are that it is fixed, so the mask itself never contributes a diff, and that it is obvious in a review, so a reviewer looking at a failing capture immediately understands which regions were excluded rather than wondering why an area shows no change.

Do masks need to be applied at capture or at comparison?

Both, driven by the same manifest, for the reason set out in Interactive Overlay Masking Rules: a runner without native masking still produces a comparable result if the comparator zeroes the same rectangles. Applying at only one of the two produces failures that reproduce on some runners and not others.

How do I stop the mask list growing forever?

The expiry date in the manifest, and an audit that reports expired entries as failures rather than warnings. In practice a third of masks turn out to be workarounds for determinism gaps that were fixed elsewhere months earlier, and nothing but a scheduled prompt ever causes anyone to check.