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.

Where a live overlay's variance enters, compared with an animated layer Two chains are drawn side by side. The animated layer chain runs from a clock inside the page to a frame index to the rendered layer, and every link is inside the page boundary, so a substitution reaches all of them. The live overlay chain runs from an external producer, over a transport, through a client buffer, into application state and then the rendered layer; only the last two links are inside the page boundary. A vertical line marks that boundary, showing that the two upstream sources of variance — when the producer emits and how the transport delivers — cannot be substituted from page scope and must be replaced at the transport instead. The variance enters upstream of anything a page script can reach animated layer live overlay clock in the page frame index rendered layer external producer transport client buffer → layer the fix has to sit at the transport, because that is the first link the test controls

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.

What each stabilization approach leaves under test Three approaches are shown as bars whose filled length represents how much of the rendering path remains under test. Freezing the feed at the transport leaves parsing, projection, symbol selection, collision and clustering all executing, and removes only the arrival of new data. Replaying a recorded window additionally keeps the state-merge and transition path under test but requires ordered fixtures. Excluding the region leaves nothing about the overlay under test at all and is only appropriate when the overlay is outside the suite's scope. Beside each bar is the assertion class it supports. How much of the overlay each approach keeps under test freeze the feed replay a window exclude the region parse, project, symbolise, collide, cluster — supports “is the data drawn correctly?” all of the above plus state merge and marker transitions — supports “does it respond correctly?” nothing — supports no assertion about the overlay at all start at the top and move down only when the row above is genuinely unavailable

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.

Feed timestamps against a frozen page clock, before and after the rewrite Two timelines share an age axis running from fresh on the left to expired on the right, with a styling threshold marked partway along. In the first timeline the recorded fixture's timestamps sit at their original instants while the page clock is frozen at a different epoch, so every feature computes an age far past the expiry threshold and the overlay renders empty — a capture that is perfectly stable and completely wrong. In the second timeline the fixture's timestamps have been rewritten to fixed offsets from the frozen epoch, so features land deliberately on both sides of the threshold: some fresh, some faded, one expired, exactly as the test intends. A frozen clock plus recorded timestamps renders an empty layer as recorded rewritten expiry threshold every feature past expiry → layer renders empty fresh and faded, deliberately placed one expired, on purpose rewriting the offsets makes age-based styling a property of the fixture rather than of the recording date

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 EventSource replaced instead of WebSocket. 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 304 afterwards, 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.