July 11, 2026
What to Log When Browser Tests Fail Only After a WebSocket Reconnect
A practical checklist for debugging browser tests that fail after WebSocket reconnects, with logging advice for real-time apps, browser test logs, and flaky test triage.
Browser tests that only fail after a WebSocket reconnect are frustrating for a simple reason: the UI often looks fine right up until the connection drops and comes back. Then a badge disappears, a toast reappears, a list refreshes, or a button becomes briefly disabled, and your test is left staring at a state that is technically valid but not the one it expected.
These failures are rarely caused by a single missing wait. More often they are the result of event ordering, stale UI state, race conditions between connection lifecycle and rendering, or test infrastructure that does not capture enough context to explain what happened. If you are trying to reduce the number of times your team says “it only fails in CI” for a real-time app, the right logs matter more than more retries.
This checklist focuses on what to record when browser tests fail after websocket reconnect. It is written for teams using Playwright, Selenium, Cypress, or similar browser automation tools, and it assumes you are testing something with live connection behavior, like chat, collaborative editing, dashboards, trading screens, streaming updates, or presence indicators.
Why reconnect-specific failures are different
A reconnect event is not just a network issue. It is usually a protocol-level transition that may trigger:
- a new socket handshake,
- reauthentication or token refresh,
- resubscription to channels,
- replay of missed messages,
- state reconciliation in the UI,
- temporary loading or disconnected banners,
- DOM replacement for live components.
That means a browser test can fail in several distinct ways:
- it checks too early, before the UI has resynced,
- it checks too late, after a reconnect banner has disappeared,
- it finds a stale element that was replaced during rerender,
- it misses a message that was replayed on reconnect,
- it assumes exactly one state transition when the app emits several.
The logging goal is not just to say “the reconnect happened.” It is to answer, in order, what disconnected, what reconnected, what the client thought happened, what the server sent, and what the UI rendered.
If your logs cannot reconstruct that chain, you are debugging a guess, not a failure.
Log the connection lifecycle, not just the failure
The first rule is to treat the WebSocket lifecycle as test evidence. Log the same milestones every time so the failure output is comparable across runs.
Record these core events
At minimum, capture:
- connection opened,
- connection closed,
- close code and reason,
- reconnect attempt number,
- reconnect delay or backoff value,
- handshake start and end,
- authentication refresh if applicable,
- subscription or resubscription completion,
- first message after reconnect,
- UI recovered state.
If your app uses a wrapper around native WebSocket, log the wrapper events too. If the reconnect is handled by a library, log its internal state transitions if they are exposed.
A useful pattern is to correlate every event with a monotonic timestamp and a run-specific connection ID.
function logWs(event: string, data: Record<string, unknown> = {}) {
console.log(JSON.stringify({
ts: Date.now(),
event,
...data,
}));
}
ws.addEventListener(‘open’, () => logWs(‘ws_open’)); ws.addEventListener(‘close’, (e) => logWs(‘ws_close’, { code: e.code, reason: e.reason, wasClean: e.wasClean, }));
You do not need this exact format, but you do need a structure that is easy to grep, parse, or ship to a test artifact.
Capture the reason for the disconnect
Not all reconnects are equal. The root cause changes how you debug the test.
Log the close code and reason
For browser-connected apps, the close event often exposes enough context to separate normal reconnect behavior from real transport problems. Record:
codereasonwasClean- whether the browser reported a network error before close
This helps distinguish cases like:
- server intentionally closing due to token expiry,
- idle timeout from a proxy or load balancer,
- mobile-style network interruption,
- application-level logout,
- abrupt process restart on the backend.
If your app uses HTTP polling as a fallback, log the exact transition to the fallback channel as well. A test that expects a websocket reconnect may actually be failing because the app silently downgraded to another transport.
Include environment-level hints
Capture whether the test ran behind:
- a corporate proxy,
- a local mock server,
- a container network bridge,
- a service mesh,
- a cloud load balancer with idle timeout behavior.
That context matters because reconnect flakiness can be caused by the path between browser and server, not the app code itself.
Log timing around the reconnect window
A lot of false failures come from a narrow timing gap. The app may show a disconnected indicator for a few hundred milliseconds, then immediately resubscribe and re-render. If your assertion checks the wrong slice of time, it will fail randomly.
Measure these timestamps
Log the time of:
- last message before disconnect,
- connection close,
- reconnect attempt start,
- reconnect success,
- resubscription completion,
- first message received after reconnect,
- UI state stabilized.
If you use Playwright, log browser-side timestamps alongside test-side timestamps so you can compare them.
typescript
await page.exposeFunction('testLog', (event: string, payload: unknown) => {
console.log(JSON.stringify({ ts: Date.now(), event, payload }));
});
await page.evaluate(() => { (window as any).testLog(‘ui_state’, { online: navigator.onLine, readyState: (window as any).socket?.readyState, }); });
The key is to find the delta between reconnect and the moment your UI becomes testable again. If that delta varies a lot, you likely have a synchronization problem, not a product bug.
Log the subscription state after reconnect
A socket can reconnect successfully and still leave the app in a broken state if subscriptions are not restored correctly. This is one of the most common causes of reconnect-related flakiness.
Record subscription details
When reconnecting, log:
- channel or topic name,
- subscription ID,
- filter parameters,
- auth token version or claim set if relevant,
- ack or confirmation from the server,
- replay cursor, sequence number, or last seen event ID,
- whether replay was requested or delivered.
If your UI depends on specific streams, log which ones were active before disconnect and which ones were active after reconnect. This helps detect “partial recovery,” where the socket is up but one data stream never resumed.
A reconnect that restores the transport but not the subscription is a very different bug from a dead socket.
Check for duplicated subscriptions
Another failure mode is duplicate subscriptions after reconnect. The test may see doubled messages, duplicated rows, or repeated notifications. Log whether the reconnect path reuses a subscription ID or creates a new one, and whether the server acknowledges cleanup of the old one.
Log message sequence, not just message content
Real-time UI tests often need to assert that events arrived in order. When reconnect happens, the order can change because of replay, buffering, or server-side deduplication.
Include sequence metadata
For each message, capture:
- message ID,
- sequence number,
- source channel,
- whether it was live or replayed,
- server timestamp,
- client receive timestamp,
- any gap detected by the client.
This is more useful than logging raw JSON payloads alone. A payload can be correct but arrive too late, out of order, or twice.
Watch for client-side dedupe logic
If the app deduplicates messages after reconnect, log:
- the dedupe key,
- whether the event was ignored,
- whether the event replaced an existing item,
- whether the event updated a hidden cache.
Without that, you may mistake a legitimate dedupe decision for a missed update.
Log what the browser saw, not only what the test saw
Browser automation logs are often too narrow. They see assertions and failures, but not the page’s own internal state.
Capture browser console output
Browser console logs are often the fastest way to understand reconnect behavior. Record:
- connection state transitions,
- retry counters,
- subscription messages,
- auth refresh warnings,
- UI recovery notices,
- errors swallowed by the app.
In Playwright, you can listen for console events and page errors.
page.on('console', (msg) => {
console.log(JSON.stringify({
ts: Date.now(),
type: 'console',
text: msg.text(),
level: msg.type(),
}));
});
page.on(‘pageerror’, (err) => { console.log(JSON.stringify({ ts: Date.now(), type: ‘pageerror’, message: err.message, })); });
Log network-level browser events too
If your tooling allows it, capture request and response traces around the reconnect window. This is useful when a reconnect depends on a token refresh endpoint, config fetch, or channel bootstrap request.
For Selenium-based setups, you may need to rely on browser logs, proxy logs, or application instrumentation, because raw WebSocket inspection is less direct than in Playwright.
Log DOM changes that happen during reconnect
When a reconnect triggers a rerender, the failure is often not the socket itself. The problem is the DOM transition.
Capture the elements that matter
Log whether the following changed after reconnect:
- loading spinners,
- offline or reconnect banners,
- disabled buttons,
- unread counters,
- table rows,
- message lists,
- optimistic UI placeholders,
- focus state.
If a test clicks a button and the page replaces that button during reconnect, you may get a stale element failure, a detached node error, or a click on a new element with different state.
A simple DOM snapshot around the sensitive region is often enough.
typescript
const status = await page.locator('[data-testid="connection-status"]').textContent();
const count = await page.locator('[data-testid="message-row"]').count();
console.log(JSON.stringify({ ts: Date.now(), status, count }));
If the page uses virtualization, log the logical item count separately from the visible row count. Otherwise you will confuse viewport behavior with data loss.
Log the test action that preceded the reconnect
A reconnect is often triggered by something the test did, even if indirectly.
Track the last user action and last assertion
Always log the sequence:
- last UI action,
- last server action if the test sets up fixtures,
- last assertion that passed,
- test step in progress when disconnect occurred.
This helps answer whether the failure started during setup, during the reconnect itself, or during the validation after reconnect.
For example, a test might:
- create a collaborative document,
- type text,
- simulate network loss,
- wait for reconnect,
- verify the text is still present.
If it fails, the relevant log is not just “socket reconnected.” You need to know whether the text update was actually acknowledged before the disconnect.
Log auth and session state around reconnect
Many reconnect issues are really session issues. A reconnect can fail because the client lost its token, refreshed it too late, or resumed with stale credentials.
Capture session details carefully
Log:
- access token refresh time,
- token expiration relative to reconnect,
- auth endpoint status,
- session user or tenant ID,
- whether reconnect used a new token,
- whether the server accepted the resumed session.
Do not log full secrets in test artifacts. Log token fingerprints, expiry timestamps, or a redacted claim summary instead.
If reconnect happens near token expiry, you want to know whether the app refreshed the token before reconnect, during reconnect, or only after a failed handshake.
Log backend correlation IDs when available
The most useful browser test logs often come from joining browser-side events to backend request logs.
Propagate a run ID
Add a test-run correlation ID to:
- the initial page load,
- the websocket handshake if your stack supports it,
- reconnect-related HTTP calls,
- server logs,
- client telemetry.
A lightweight pattern is to inject a unique test run ID into local storage, query params, or a test-only header. The server can then echo it back in logs.
# GitHub Actions example of passing a run ID into tests
name: e2e
on: [push]
jobs:
browser-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
env:
TEST_RUN_ID: $-$
That one variable can save hours when you need to match browser test logs to server traces.
Log infrastructure and network conditions used by the test
Reconnect flakiness is often caused by the environment the test runs in, not the app code.
Record the test environment shape
Include:
- browser name and version,
- headless or headed mode,
- operating system,
- container image tag,
- CPU and memory limits if containerized,
- network shaping or fault injection settings,
- proxy configuration,
- grid node or worker ID,
- region or availability zone if relevant.
This is especially important for browser testing on Selenium Grid or other distributed runners, where intermittent network instability can be masked by retries.
The concept of software testing is broad, but for reconnect problems you want something narrower: enough telemetry to reproduce the failure on a node with the same constraints.
Distinguish transport reconnect from UI recovery
Teams often log that a socket reconnected and assume the test should proceed. That is not enough.
Track two separate states
Log both:
- transport state, for example connected, disconnected, reconnecting,
- UI readiness state, for example loading, synced, interactive.
These states can diverge. The socket may be connected while the UI still waits for replay or hydration. Conversely, the UI may show old data while the transport is already healthy.
Your test should assert on the state that matters. If the goal is live connection testing, then “socket open” is not sufficient. You may need an explicit “subscriptions restored and initial sync complete” state.
Add application-level markers for stabilization
The best logs are often emitted by the application, not the automation tool.
Create a test-friendly readiness signal
Consider adding a clear, test-only, or diagnostics-friendly marker such as:
window.__APP_READY__ = true,- a DOM attribute like
data-test-sync-state="ready", - a status event emitted after resubscription completes,
- a specific console message after replay is applied.
This makes it easier to wait for the right condition instead of polling the whole DOM and hoping it settles.
The point is not to weaken your test. It is to make the application’s real readiness observable, which is a core principle of test automation.
A practical logging checklist for reconnect failures
Use this as a minimum checklist when a browser test fails only after a WebSocket reconnect:
Connection lifecycle
- open event timestamp
- close event timestamp
- close code and reason
- reconnect attempt count
- reconnect backoff timing
- final connected timestamp
Subscription and data flow
- channels or topics subscribed
- resubscription success or failure
- replay cursor or last event ID
- first post-reconnect message ID
- duplicate or skipped event notes
UI state
- connection banner text or visibility
- loading state at reconnect
- elements re-rendered or replaced
- stale element or detached node errors
- final readiness marker
Auth and session
- token refresh time
- auth failure or success
- session identity summary
- expiry timing relative to reconnect
Browser and environment
- browser version
- headless or headed
- grid worker or container ID
- network proxy or shaping details
- test-run correlation ID
Evidence attached to the failure
- browser console logs
- page errors
- network traces around reconnect
- screenshots or DOM snapshots at failure time
- server logs joined by correlation ID
Example: a brittle assertion versus a useful one
A brittle test might do this:
typescript
await expect(page.locator('[data-testid="message-count"]')).toHaveText('3');
That works only if the reconnect path is always instantaneous and the UI is always synchronized by the time the assertion runs.
A better approach is to wait for a visible recovery signal and log the relevant events:
typescript
await expect(page.locator('[data-testid="connection-status"]')).toHaveText('Connected');
await expect(page.locator('[data-testid="sync-state"]')).toHaveText('Ready');
console.log('reconnect recovered, checking message count');
await expect(page.locator('[data-testid="message-count"]')).toHaveText('3');
The point is not that status elements are magical. The point is that the test validates the same recovery condition a real user depends on.
What not to log
It is easy to over-log and still miss the important signals.
Avoid these mistakes:
- logging raw websocket payloads without sequence metadata,
- logging every heartbeat without a way to correlate them,
- storing secrets, bearer tokens, or PII in artifacts,
- logging only the final assertion failure and nothing before it,
- logging UI text but not network or reconnect state,
- depending on screenshots alone when the state change is invisible.
Logs should help answer a question. If they create noise without causality, they are not helping.
When to move from logs to instrumentation
Sometimes logging is enough, and sometimes it is not.
You likely need deeper instrumentation if:
- the reconnect happens too quickly to capture reliably,
- the same failure appears with different symptoms,
- the application has multiple live channels and only one fails,
- the UI recovers visually but internal state is wrong,
- the issue only appears under load or in CI.
At that point, add explicit diagnostic events in the app or test harness. Good browser test logs are a form of observability, not a substitute for it.
Closing checklist for teams
If your browser tests fail after a websocket reconnect, start by answering these questions with logs:
- Did the socket close cleanly, and why?
- How long did reconnect take?
- Were subscriptions restored?
- Was any replay missing, duplicated, or out of order?
- Did the UI rerender, and did it reach a ready state?
- Did auth or session state change during the reconnect?
- Can you join browser events to backend logs with a run ID?
- Did the test assert on transport state or real application readiness?
That list sounds simple, but it is usually enough to turn a flaky mystery into a reproducible defect.
For teams doing serious continuous integration, the practical goal is not to eliminate every reconnect, because real users experience them too. The goal is to make reconnects observable enough that your browser tests can tell the difference between a temporary recovery and a real bug.
If your current logs only say “failed after reconnect,” they are not enough yet. Start by logging the lifecycle, the subscriptions, the UI recovery signal, and the correlation IDs. In most cases, that is the shortest path from reconnect flakiness to a stable test suite.