Build1 publisher3 min readPublished
Playwright's actionability check scrolled past the overflow that trapped real users
An element with overflow: hidden still has a live scrollTop, so click() moved a clipped blog form 1200 pixels while the wheel and the End key moved it zero. All the suite asserted was the click.
The Engineer · Build desk
What happened
- A blog editor shipped inside a dashboard rendered with its form clipped: the body field half visible, the FAQ section and publish options off screen, and neither the mouse wheel nor Page Down doing anything.
- The end-to-end test for that same form passed every step, filling it, clicking "Add question" at the very bottom, publishing the post and confirming it appeared on the public blog.
- A standalone Playwright script reproduced it, printing user scrollTop: 0 after a wheel event and an End keypress, then clicked: true and test scrollTop: 1200 after a single click() call.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Because the driver scrolls targets into view from code, an interaction assertion cannot fail on overflow containment, so a whole class of layout regressions is invisible to a passing suite.
- decision Suites that test full-height layouts now have to assert which container the page mounted in and that it scrolls, because asserting the button was clicked proves only that the driver could reach it.
- cost The green test led to a wrong diagnosis: a widened editor shipped to production and the users still could not reach the bottom of the form.
An element with `overflow: hidden` is still a scroll container. It clips its content and hides the scrollbar, and it ignores the wheel and the keyboard. Its `scrollTop` stays live, so code that writes to it works, as does `scrollIntoView()`, as does focus moving to something inside it [7]. Playwright's `click()` runs actionability checks before it clicks, and one of them scrolls the target into view from code [8]. That scroll goes straight through an `overflow: hidden` ancestor.
The author of the dev.to post measured that instead of trusting the docs. The test page is a shell at `height: 100vh` with `overflow: hidden`, a form 2000px tall, a button positioned at `top: 1800px`, and a viewport of 1200 by 800 [9]. A 1500-pixel wheel event and an `End` keypress left the shell's `scrollTop` at 0. Then `click()` reported the button clicked and left `scrollTop` at 1200 [10][11]. An 800-pixel shell holding a 2000-pixel form has exactly 1200 pixels of scroll range, so the driver went to the very bottom of a container the user could not move [1]. The button ended up 600 pixels down an 800-pixel viewport [2].
That covers Chromium through Playwright, on one synthetic page, at one viewport size [9]. Two things have to hold for the result to transfer to another suite: the driver's pre-click scroll has to be programmatic rather than input-driven, and the clipped ancestor has to be taller than the box it sits in. The post did not test other drivers.
The defect underneath is the condition that chooses between the full-height shell and the normal scrolling container: `!billing && !profile && !admin && mode === "person" && nav === "inbox-mail"` [4]. Every negation in there is a page somebody remembered. The blog editor was added to the router and not to that list, so it rendered inside the fixed shell with `overflow: hidden`, taller than the screen and clipped [5]. The fix was `&& !blog` and a comment on the line telling the next person to add new pages there [6].
The other layers all let it through. The unit tests do not render the layout, the production build does not care about CSS, and the end-to-end suite drove the real form in a real browser [12]. "From the test's point of view nothing was wrong, because for the test nothing was," the author wrote [16].
The first fix was a misdiagnosis. The author read a cramped form as a width problem, widened the editor, shipped it, and the form was still cut off [13]. The author's point was that a user saying they cannot get to the bottom of the form is describing a scrolling problem, not a width problem [14].
The assertion that replaced it is about the container. It checks that the blog page appears exactly once inside `.content`, that it does not appear inside `.content-fill`, and that the container it landed in really scrolls [15].
What to watch
- Whether Playwright adds an option to run actionability scrolling the way a user scrolls, so a clipped ancestor fails the click instead of being scrolled through.
- Whether the dashboard replaces the exclusion list with per-route opt-in, so a new page cannot land in the full-height shell by default.
- Whether the same 0-versus-1200 split reproduces on other drivers, since the post measured Chromium through Playwright only.