Live Data Overlay Stabilization
A basemap can be pinned completely and the capture will still differ on every run, because the interesting layer on top of it is fed by something that keeps moving: vehicle positions arriving over a socket, sensor readings polled every fifteen seconds, incident markers appearing and clearing on their own schedule. The data is the product, so it cannot simply be removed, and it is not an animation, so none of the animation-suppression machinery reaches it. Stabilizing a live overlay is a distinct problem with its own techniques, and this page works through them from the transport inward.
This page sits within Dynamic Element Masking & UI Stability and is the data-feed counterpart to Animated Tile Layer Stabilization: that page freezes a layer whose frames come from a clock, this one freezes a layer whose frames come from a network feed.
What makes a live overlay different from an animation
An animated layer is driven by something inside the page — a timer, a frame loop, a tile URL carrying a timestamp — and every one of those is reachable with a substitution. A live overlay is driven by something outside the page, and the page has no authority over it at all.
That difference has three practical consequences.
The arrival order is not the render order, and neither is stable. Two runs against the same recorded feed can deliver the same twelve updates in a different sequence, and if the overlay renders in arrival order, the z-stacking of overlapping markers differs. Freezing the clock does nothing about this, because the variance is in the transport rather than in time.
The data volume varies with the run. A socket that pushes updates continuously delivers however many messages happen to arrive before the capture gate opens. A slower runner receives more of them. The number of vehicles on screen therefore correlates with machine load, which is as close to a perfectly untraceable flake source as a suite can have.
The content is the assertion. With a decorative animation, masking the region costs nothing of value. With a live overlay, the region is what the test exists to check — that vehicles render at the right coordinates, that incident icons use the right symbol, that a cluster badge shows the right count. Masking it removes the reason for the test.
Design patterns: freeze, replay, or exclude
Three approaches cover the space, and they differ in how much of the overlay stays under test.
Freeze the feed at the transport. The test intercepts the socket or the poll endpoint and serves one recorded snapshot, then closes or stalls the connection. The overlay renders exactly the features in the snapshot, in the order the snapshot lists them, and never updates. This is the highest-fidelity option: every part of the rendering path — parsing, projection, symbol selection, collision, clustering — still executes on real data, and only the arrival of new data is removed. It suits any assertion about how data is drawn.
Replay a recorded window deterministically. The test serves a recorded sequence of messages with the timing removed, driving them into the client in a fixed order and then waiting for quiescence. This keeps the update path under test — the diff logic that merges an update into existing state, the transition a marker plays when it moves — at the cost of more fixture machinery. It suits assertions about how the overlay responds to data rather than how it draws it.
Exclude the region. The overlay is masked and the test asserts on everything else. This is the right answer only when the overlay’s content is genuinely outside the scope of the visual suite — a third-party widget, a feed you cannot record — and it should be a documented decision in the masking manifest described in Interactive Overlay Masking Rules rather than a default.
Step-by-step: freezing a socket-fed vehicle layer
The procedure below assumes the overlay reads from a WebSocket and renders into a GeoJSON source. The same shape applies to a polled REST endpoint with the route interception swapped in for the socket stub.
1. Record one representative window from the real feed. Capture the raw messages for thirty seconds against the staging environment, including the initial snapshot the server sends on connect. Store it as a fixture next to the test, and record the wall-clock instant it was taken — the feed’s own timestamps will need to agree with the frozen clock later.
2. Replace the socket before the application constructs it. Because the application opens its socket during startup, the substitution has to be installed in an init script, for the reasons set out in Playwright Event Hooks for Map Capture.
await context.addInitScript((frames) => {
class StubSocket extends EventTarget {
constructor(url) {
super();
this.url = url;
this.readyState = 1;
queueMicrotask(() => {
this.dispatchEvent(new Event('open'));
for (const frame of frames) {
this.dispatchEvent(
new MessageEvent('message', { data: JSON.stringify(frame) })
);
}
window.__feedDrained = true; // the readiness predicate reads this
});
}
send() {}
close() { this.readyState = 3; }
}
window.WebSocket = StubSocket;
}, RECORDED_FRAMES);
Dispatching every frame in one microtask, rather than on timers, is what makes the result deterministic: the client sees the whole window at once and settles into a single final state, with no dependence on how fast the runner is.
3. Sort before rendering, not after. If the overlay writes features into a source in the order they arrived, add an explicit sort by feature id at the point the source is updated. This is a change to the application, not to the test, and it is worth making regardless: an overlay whose z-order depends on network arrival order has a real rendering bug that users see as markers flickering in front of each other.
4. Extend the readiness predicate to include the feed. The composite predicate from WebGL Idle & Render-Completion Detection gains one more clause.
const settled = () =>
window.__feedDrained &&
map.areTilesLoaded() &&
!map.isMoving() &&
quietFrames >= 2;
5. Reconcile the feed’s timestamps with the frozen clock. Live overlays commonly style features by age — a vehicle reported two minutes ago draws faded, one reported ten minutes ago is dropped. With Date.now() frozen at an epoch and the fixture recorded at a different instant, every feature in the fixture is either impossibly fresh or long expired. Rewrite the timestamps in the fixture at load time so they sit at fixed offsets from the frozen epoch, and the age-based styling becomes a deterministic function of the fixture rather than of when it was recorded.
6. Assert the feature count before capturing. The single most useful guard on a live overlay is a check that the rendered feature count matches the fixture’s. It catches a dropped message, an over-eager expiry rule and a silently failed parse, all of which otherwise present as a slightly different picture that a tolerance might absorb.
Reconciling feed time with frozen time
Step 5 is the one most teams discover late, so it is worth drawing. A live overlay usually carries three independent notions of time, and freezing the page clock changes the relationship between them in a way that silently alters what renders.
The rewrite itself is three lines applied when the fixture loads, and the value it adds is that a reviewer reading the fixture can see which features are meant to be faded and which are meant to be gone. Without it, the same fixture produces a different picture in six months’ time for reasons that have nothing to do with the code.
Cross-browser and cross-environment considerations
- Socket substitution is engine-independent because it replaces a JavaScript global, so it behaves the same on Chromium, WebKit and Firefox. This makes a frozen live overlay one of the easier things to run across the whole Cross-Browser Baseline Matrix.
- Server-sent events need the same treatment as sockets, with
EventSourcereplaced instead ofWebSocket. Applications frequently use both, with a fallback path — stub both, or the fallback quietly reaches the network on the engine where the primary transport is unavailable. - Polled endpoints are simpler and easier to get wrong. Route interception handles them, but a poll that continues after the first response keeps firing, and each response re-renders. Serve the fixture once and return
304afterwards, or the overlay never reaches a quiescent state. - Time-based styling multiplies with the timezone. If the overlay formats a timestamp for display, the container’s timezone is now part of the capture’s input vector; pin it in the runner image alongside the fonts.
Deciding what the overlay test is actually for
Before choosing a stabilization technique it is worth being explicit about which question the capture is meant to answer, because the three approaches above answer different ones and teams routinely pick the wrong one by default.
If the question is “does our symbology render correctly” — the right icon for each incident category, the right colour ramp for a sensor value, labels that do not collide with the basemap’s — then a frozen feed carrying a handful of deliberately chosen features is ideal, and a recorded window from the real feed is actively worse: it delivers whatever categories happened to occur that afternoon, so the test’s coverage of the symbology depends on the weather.
If the question is “does the overlay handle a realistic load” — that two hundred markers at zoom 12 still cluster sensibly, that label collision does not thin the layer to nothing — then the fixture should be sized rather than curated, and generated to a target density rather than recorded.
If the question is “does an update apply correctly” — that a moved vehicle re-renders in the new position rather than duplicating, that a cleared incident disappears — then a replayed window is the only approach that reaches it, and the capture should be taken at a named step in the replay rather than at the end.
Writing that sentence down in the test file, in one line, is what stops the fixture drifting into something that no longer serves the assertion it was created for. It also tells the next engineer whether adding a feature to the fixture is helpful or harmful, which is not otherwise obvious from reading either the test or the data.
Feed stabilization reference
| Parameter | Recommended value | Rationale |
|---|---|---|
| Recorded window length | 20–40 s | Long enough to include a full update cycle for most feeds; short enough to keep the fixture reviewable |
| Frame delivery | all in one microtask | Removes runner speed from the result entirely |
| Feature ordering | explicit sort by id | Arrival order is not stable and is not a rendering contract |
| Timestamp rewrite | fixed offsets from the frozen epoch | Makes age-based styling a function of the fixture |
| Feature-count assertion | exact match | Catches dropped messages that a pixel tolerance would absorb |
| Poll response after the first | 304 Not Modified |
Lets the overlay reach quiescence instead of re-rendering forever |
| Fixture refresh cadence | quarterly, or on a feed schema change | Stale fixtures stop representing the feed and quietly reduce coverage |
Common pitfalls
Stubbing the socket but not the initial REST hydration. Many live overlays fetch a snapshot over HTTP on load and then subscribe for deltas. Stubbing only the socket leaves the snapshot coming from the network, so the overlay’s baseline state is whatever the staging environment held that morning.
Delivering recorded frames on their recorded timings. It feels more faithful and it reintroduces exactly the variance the fixture was meant to remove, because the readiness gate now races the replay schedule. Faithfulness to the content is what matters; faithfulness to the timing is what breaks the suite.
Leaving the feature count unasserted. A silent parse failure on one message produces a capture with nineteen vehicles instead of twenty, which is a small pixel difference in a large frame and lands comfortably inside any sensible tolerance. The count assertion is one line and catches a class of fault that pixels cannot.
Masking the overlay as a first move. It makes the suite green immediately and removes the only part of the frame the application’s own team is responsible for. If the overlay must be masked, the mask belongs in the manifest with a reason and an owner, per the practice described under Interactive Overlay Masking Rules.
Forgetting that a frozen feed still ages. An overlay that expires features older than five minutes, driven by a frozen clock, never expires anything — which is deterministic and also not what production does. If expiry behaviour is part of what the suite should check, it needs its own fixture with timestamps placed deliberately on both sides of the threshold.
Frequently asked questions
Should the recorded fixture come from production or staging?
Staging, with production used only to confirm the message shape is the same. A production recording carries real vehicle identifiers, real positions and often real personal data into a repository that many people can read, and it gains nothing in return — the rendering path does not care whether the coordinates are genuine. Record from staging, or generate the fixture synthetically from the feed’s schema.
How large should a feed fixture be allowed to get?
Small enough that a reviewer can open it. A thirty-second window of a busy vehicle feed can be several megabytes, which is unreviewable and slow to load in every test. Trim it to the features within the camera’s bounds at the test’s zoom — the overlay renders nothing else — and the fixture typically drops by an order of magnitude with no change to the captured frame.
What if the overlay legitimately has no stable state?
Then the test should assert on something other than the whole frame. A feed that genuinely never quiesces — a continuous animation of vehicle movement, for instance — can still be captured deterministically by driving it to a specific, named state: apply exactly N updates, then stop. The state is chosen rather than waited for, and the assertion becomes about that state rather than about whatever the feed happened to be doing.
Does freezing the feed hide real regressions in the live path?
It hides regressions in the transport and reconnection logic, which is why those belong in integration tests rather than in a visual suite. Everything downstream of the message — parsing, projection, symbolisation, collision, clustering, styling — still executes on the fixture, and that is where visual regressions actually occur. The split is the same one that keeps the visual gate fast and the integration suite meaningful.
How does this interact with marker clustering?
Directly, and it is a common source of confusion. A clustered overlay’s badge counts are a function of the feature set, so a feed that delivers a different number of features produces different badges, which reads as a clustering regression. Freezing the feed removes that, and the remaining clustering determinism work — radius, maxZoom, integer zoom, insertion order — is covered in Marker Cluster Stability.
Related
- Up to Dynamic Element Masking & UI Stability, the section this topic belongs to.
- Animated Tile Layer Stabilization — the clock-driven counterpart to a feed-driven layer.
- Marker Cluster Stability — keeping cluster badges stable once the feature set is frozen.
- Playwright Event Hooks for Map Capture — where the transport substitution has to be installed.
- Interactive Overlay Masking Rules — the manifest that records a mask when exclusion really is the answer.