Your date picker passes every Playwright component test but is clipped in the real app: what did the mount miss?
answer
- The harness had no ancestors
- Clipping and stacking come from parents
- Global CSS lives in the app shell
- One component cannot show a collision
- Change the level, not the timeout
basics
~20 sThe mount renders the picker in a bare harness page, so the real app's ancestors never existed: no overflow-hidden scroll container, no stacking context, no narrow column. Recreate the ancestor in the story, or cover the case against the assembled app.
solid answer
~50 sClipping is a property of ancestors, and a mounted run has almost none. The harness page holds one component with whatever wrapper the story supplies, so an `overflow: hidden` scroll container, a `transform` that creates a containing block for a fixed popover, a `z-index` stacking context or a narrower grid column all vanish from the test. The engine was real; the surroundings were not. The fix has two halves: make the story lie less by wrapping the picker in the ancestor whose geometry it depends on, and keep a thin set of checks against the assembled application for what a single component can never show — collisions with a toast, focus moving between components, the real theme and font metrics. Reaching for a longer timeout or another component test instead is the classic wrong turn: neither adds the missing ancestor.
code
typescript · 11 linesimport { test, expect } from '@playwright/test';
test('the calendar is not clipped inside a scrolling grid', async ({ mount, page }) => {
const picker = await mount('design-system/date-picker--inside-scroll-container');
await picker.getByRole('button', { name: 'Open calendar' }).click();
const calendar = page.getByRole('dialog', { name: 'Choose a date' });
await expect(calendar).toBeVisible();
await expect(calendar.getByRole('button', { name: '15' })).toBeInViewport();
});go deeper
Take away the mechanism: clipping comes from a parent element, and the harness page has almost no parents, so a mounted run can look right while the app looks wrong.
Be able to name the ancestor properties involved, such as overflow, transform, z-index and available width, and explain why none of them exists in a bare harness page.
Show the triage: reproduce in the app, identify the responsible ancestor, then add the check at the level that can actually see it rather than adding another mount.
Own the standard. Decide how much shell a story may reproduce before it becomes a second application, and which risks the team accepts covering only against the assembled app.
## Why the mount was green A mounted component run gives you a real engine and a fake page. The date picker was rendered by Chromium, its CSS really cascaded and its calendar really opened — but it opened into a harness page whose body contained essentially nothing else. Clipping, stacking and containing blocks are all **relationships with ancestors**, and in the harness the picker has no ancestors worth the name. In the application it sits inside a data grid's scroll container with `overflow: hidden`, inside a column that is 320px wide, inside an element with a `transform` that turns it into the containing block for anything positioned `fixed`. Every one of those changes where the calendar can be drawn, and none of them was present when the test passed. ## The classes of defect a single mount structurally misses - **Clipping ancestors** — `overflow: hidden` or `overflow: auto` on a scroll container cutting the popover at its edge. - **Stacking and containing blocks** — a `z-index` context or a `transform`/`filter` ancestor that re-parents a fixed-position layer. - **Global CSS** — resets, box-sizing, theme tokens and font faces that live in the application shell rather than in the component's own stylesheet, changing metrics and therefore wrapping and height. - **Available width** — a component that has the whole viewport in the harness and a narrow column in the app. - **Collisions** — a toast, a sticky header or a modal overlay that only exists once the real screen assembles several components together. - **Real data** — label lengths, locale formats and row counts that the story's tidy fixture never produced. - **Router and global state** — a picker whose value comes from URL state or a shared store that the harness supplies as a constant. | Symptom in the app | Why the mount stayed green | Where to cover it | |---|---|---| | Calendar cut off at a container edge | No clipping ancestor in the harness | Story that reproduces the ancestor | | Calendar renders behind the header | No stacking context above it | Story with the same z-index context | | Text wraps to three lines | App fonts and tokens absent | Story that loads the shell's theme | | Confirm button unclickable | The toast lives on another screen | Assembled-app run | | Wrong date format | Locale comes from a provider | Story with the provider, or app run | ## Triage when this happens 1. **Reproduce in the app first**, and note the exact ancestor chain from the picker up to `<body>` in the browser's element inspector. 2. **Name the responsible ancestor property** — overflow, transform, z-index, width, font — rather than settling for "it looks wrong". 3. **Decide whether the story can honestly carry it.** A single wrapper with the same overflow and width is fair; a copy of the whole application shell is not. 4. **Add the check at the level that can see the cause**: story wrapper for geometry the component owns, assembled-app run for collisions with other components. 5. **Write the regression test before the fix**, so you can prove the new level actually catches it. ## How much shell to reproduce The temptation after an incident like this is to make the story faithful — theme provider, layout grid, header, the lot. That trade goes bad quickly: a story that reconstructs the application is slow, brittle, and quietly becomes a second implementation of the app that drifts from the first. The useful boundary is **the ancestor whose geometry the component's own contract depends on**. A date picker that promises "the calendar escapes a scrolling container" should have a story with a scrolling container, because that is the component's claim. A date picker that is merely a victim of a toast on one particular screen has no such contract; that defect belongs to the screen, and the screen is the thing that should be tested. ## What not to do Two reflexes make this worse. Raising a timeout treats a geometry failure as a timing failure and buys nothing, because the calendar was never going to become unclipped. Adding more component tests raises confidence without raising coverage: every one of them runs in the same ancestor-free harness, so a hundred green mounts say exactly as much about clipping as one did. The honest response is to change the level at which the missing fact is visible — and to record the boundary, so the next person reading the suite knows what its green means.
- How much of the application shell should a story reproduce?Enough to make the assertion honest and no more: the ancestor whose geometry the component's own contract depends on, plus any provider it cannot render without. Copying the whole shell turns the story into a slow second application that drifts from the real one.
- What is the cheapest check that would have caught the clipped calendar?One run against the assembled application that opens the picker where it really lives and asserts the chosen day is in the viewport. That is a single case against real ancestors, not a rewrite of the component suite.
Rehearsing a solo in a practice room proves the part is learned. It says nothing about whether the orchestra drowns it out.
saying these in an interview costs you the question
- Blaming flakiness rather than a missing ancestor
- Raising the timeout to fix a layout problem
- Assuming the harness carries the app theme and resets
- Believing more component tests would have caught it
- Copying the entire application shell into every story