How to Debug Browser Tests That Fail Only After ResizeObserver, Container Queries, or Hydration Reflow Changes the Layout
By David Frei · October 2, 2026
A practical workflow for Playwright and Selenium tests that fail after ResizeObserver, container queries, or hydration reflow changes layout, with root-cause checks, wait patterns, and assertion fixes.
A test that passes on first paint and fails a moment later is usually not “just flaky.” It is often asserting against a layout that the page is still allowed to change. The usual suspects are ResizeObserver, CSS container queries, and client hydration, all of which can change sizes, visibility, or even the DOM after the initial render.
If your browser tests fail after ResizeObserver runs, the right question is not “how do I wait longer?” It is “what exactly changed, when did it change, and should the test assert before or after that change?” That distinction decides whether you fix the product, the test, or both.
The short diagnosis
When a test fails only after a size-dependent update, there are three broad causes:
| Symptom | Likely cause | What to check first |
|---|---|---|
| Element exists, but text, size, or position changes after a short delay | ResizeObserver or container query driven reflow |
Observe DOM mutations, layout shifts, and computed styles before asserting |
| Element appears, then disappears or re-renders during page startup | Hydration replacing server markup or re-binding UI state | Compare server HTML, hydrated DOM, and the first assertion time |
| Same test passes locally but fails in CI at one viewport | Assertion assumes a specific breakpoint or font metric | Fix the viewport, wait for the intended stable state, or assert behavior instead of pixels |
The rest of this article is a workflow for separating those cases quickly.
First, define the layout change you are chasing
These terms are related but not identical:
ResizeObserver, a browser API that notifies code when an observed element changes size. See the ResizeObserver API.- Container queries, CSS rules that react to the size of a containing element, not the viewport. See CSS container queries.
- Hydration, the client-side process that attaches behavior to server-rendered markup and may reconcile or replace parts of the DOM. In React, this is documented under hydration.
A test can fail after any of these because they all can change layout after the first paint. The important distinction is whether the page is changing because script is responding to size, because CSS is responding to size, or because the framework is reconciling markup.
If the page is still legally changing layout, a fast assertion is often checking the wrong state, not the wrong selector.
A root-cause workflow that does not guess
1) Reproduce at a fixed viewport and a single browser
Start by removing variability. Set the viewport explicitly, disable randomization in test data, and pin the browser you are debugging. A layout bug that only appears near a breakpoint can vanish if your local browser size drifts by a few pixels.
For Playwright, that usually means something like:
import { test, expect } from '@playwright/test';
test.use({ viewport: { width: 1280, height: 800 } });
test('checkout summary stays visible', async ({ page }) => {
await page.goto('/checkout');
await expect(page.getByRole('heading', { name: 'Order summary' })).toBeVisible();
});
For Selenium, make the window size explicit in your setup rather than relying on defaults.
2) Capture the first failing frame and the next repaint
A layout-dependent failure is easier to reason about if you know whether the page changed before the assertion, during the assertion, or after the assertion.
Useful probes:
- take a screenshot immediately before the failing assertion,
- inspect
getBoundingClientRect()for the target element, - log computed style for
display,visibility,position,width,height, andoverflow, - check whether the DOM node reference is being replaced.
In Playwright, a minimal diagnostic probe can look like this:
const box = await page.locator('[data-testid="summary"]').boundingBox();
console.log('summary box', box);
console.log('html', await page.locator('[data-testid="summary"]').evaluate(el => el.outerHTML));
If the box changes between runs but the selector stays valid, your problem is probably layout timing, not selector brittleness.
3) Determine whether the DOM changed, or only the layout changed
This is the fork that saves time.
- If the DOM tree changed, hydration or script-driven rendering likely replaced or re-ordered nodes.
- If the DOM stayed the same but size or position changed,
ResizeObserveror container queries are likely adjusting styles. - If the DOM and computed styles both changed, a state transition is involved, and your assertion may be too early.
A cheap way to verify this is to compare a stable attribute or node identity across a short wait. For example, if a node has the same data-testid but different inner text or dimensions, the element is the same logical target but not yet stable.
4) Read the browser console and the page errors
Hydration problems often leave clues in the console, such as mismatches between server and client output. Layout observers can also trigger console warnings if they cause repeated updates.
Do not skip this step. If the page is throwing client errors during hydration, the layout bug may be a symptom of a broken render path, not a timing issue.
5) Check whether the assertion is about behavior or pixels
This is where many flaky tests are born. A test may be trying to verify that a mobile menu opens, but it is asserting the exact width of the panel. When container queries shift the layout, the user-facing behavior may still be correct while the pixel assertion fails.
Use the narrowest assertion that proves the behavior:
- prefer visible/hidden state over exact size,
- prefer role-based text presence over absolute x/y position,
- prefer “expanded after click” over “width equals 312px,”
- use visual checks only when the layout itself is the feature.
What to change when each root cause is confirmed
If ResizeObserver is driving the change
ResizeObserver callbacks run after the browser detects a size change. If your app updates a class, swaps content, or toggles visibility from that callback, the test needs to wait for the post-observer state, not the initial paint.
Fixes:
- wait for the target element to reach a stable size,
- wait for the UI state that the callback is supposed to produce,
- avoid asserting immediately after an action that changes container size.
A useful pattern is to wait for the final state, not the observer itself:
await page.locator('[data-testid="sidebar-toggle"]').click();
await expect(page.locator('[data-testid="sidebar"]')).toHaveClass(/open/);
If the callback is expensive or loops, inspect whether the resize handler is causing a feedback cycle, for example, changing content width in a way that retriggers itself.
If container queries are involved
Container queries make component layout depend on the size of the nearest query container. That means a test can fail even when the viewport is unchanged, because a parent component or wrapper changed width.
Debugging checklist:
- identify the query container, not just the viewport,
- check whether a sibling, sidebar, or font load changed the container width,
- verify the breakpoint logic in CSS, not only in test code,
- be careful with tests that assume one global responsive state.
For example, a card grid can switch from one column to two columns when its wrapper narrows due to a new sidebar. The test should assert the card content and affordances, not an exact grid arrangement unless the grid arrangement is itself the requirement.
If hydration is the trigger
Hydration-related failures often happen when server markup and client markup do not match, or when a page renders one structure before client state is ready and another after hydration completes.
Typical signs include:
- the element is present, then replaced,
- text changes after the initial paint,
- event handlers are not ready when the test clicks,
- the page loads with placeholder content that is swapped by client code.
Practical fixes:
- wait for a post-hydration selector or state flag that represents readiness,
- assert against stable data attributes rather than transient text nodes,
- avoid clicking immediately after navigation if the route is still hydrating,
- investigate server/client render divergence in the application, not just the test.
If the test is failing because the app shows different markup on the client than the server, the test may be correctly detecting a real rendering bug.
A simple triage decision tree
Use this sequence before changing the test:
- Does the failure disappear if the viewport is fixed? If yes, inspect responsive breakpoints and container width.
- Does the failure disappear if you wait for a specific post-load state? If yes, the issue is timing.
- Does the DOM actually change, or only the box model? If the DOM changes, inspect hydration or rerender logic.
- Does the app still fail when manually reproduced at the same size? If yes, it may be a product bug, not a flaky test.
- Does the assertion check an implementation detail instead of user-visible behavior? If yes, rewrite the assertion.
Waiting longer is the last step, not the first one. The goal is to wait for the correct state, not an arbitrary pause.
The wait patterns that usually help, and the ones that hide bugs
Good waits are state-based. Bad waits are time-based.
Good examples:
- wait for a heading to be visible,
- wait for a spinner to disappear,
- wait for a button to become enabled,
- wait for a container to have a particular class or attribute.
Risky examples:
sleep(2000)after every navigation,- arbitrary retries around a failing assertion,
- waiting for network idle when the UI is driven by client timers or observers.
The reason time-based waits are dangerous here is that layout can change after the wait finishes. You may hide the race rather than remove it.
When it is a test problem, and when it is a product problem
Treat the result differently depending on what the investigation shows.
It is mostly a test problem if:
- the UI is stable after a short, well-defined readiness signal,
- the assertion is too exact for a responsive design,
- the test depends on pixel placement that is not user-critical,
- the test navigates faster than hydration or observer-driven layout can settle.
It is mostly a product problem if:
- the page visibly jumps in a way that changes the intended interaction,
- hydration produces mismatched content or broken event wiring,
- a resize callback causes a loop or repeated relayout,
- the component becomes unusable at valid widths.
If the page is genuinely unstable, the test is doing useful work by catching it.
A compact debug checklist for flaky layout failures
- Fix the viewport and browser.
- Record the exact step where the element changes.
- Compare DOM identity and computed style before and after the change.
- Inspect console warnings and hydration errors.
- Decide whether the assertion should target behavior, not layout.
- Replace sleeps with state-based waits.
- If the app itself is unstable, file a product bug with a minimal reproduction.
FAQ
Why do browser tests fail after ResizeObserver when the selector is correct?
Because the selector can still point to an element whose size, visibility, or placement changes after the observer callback runs. The selector is valid, the assertion timing is not.
How do container queries make browser tests flaky?
They let component styles change when a parent container changes width. That means the same viewport can produce different layouts if surrounding content shifts the container size.
Is hydration reflow always a test issue?
No. Sometimes the test is too early, but sometimes hydration reveals a real mismatch between server and client render output. If the DOM is replaced or event handlers are missing, investigate the application.
Should I use fixed sleeps for responsive layout tests?
Usually no. Sleeps can hide the symptom without proving the UI reached the correct state. Prefer waiting for a visible post-layout condition.
What is the best assertion style for responsive browser tests?
Assert the user outcome first, for example visible text, enabled controls, or a stable interactive state. Only assert size or position when layout is the feature under test.
The most useful habit is to treat layout changes as a first-class part of the test plan. If the page is allowed to reflow after first paint, your test needs a named readiness signal, a stable assertion target, or a product fix. Without that, every retry is just a new way to rediscover the same timing bug.