In Selenium, why can a page render narrower under headless on a build agent than on a developer's machine?
answer
- No desktop behind the browser
- The default is not your monitor
- Responsive breakpoints fire in CI
- Nothing to maximize into
- Set the size as a launch argument
basics
~20 sHeadless has no desktop to size itself against, so the browser opens at its own small fixed default window rather than at monitor size. Responsive breakpoints then fire and the page lays out as if on a small screen.
solid answer
~40 sA headed browser takes its size from the window manager and the monitor behind it; headless has neither, so it opens at a fixed default of its own — 800 by 600 in Chrome — which is far narrower than the screen you run on locally. Responsive breakpoints then fire and the page renders as a small-screen layout, hiding or relocating controls the suite expects. Resizing afterwards is a weak fix: the W3C WebDriver specification defines maximizing in terms of the operating system's window manager, and a build agent has none. Set the size at launch instead, with `--window-size=1600,1200` as a browser argument, so the first paint already uses it. In Selenium 4 that argument goes on the options object that builds every session.
go deeper
Know that headless still lays out a real page and that its window has a size. Be able to say that the size in a pipeline is not the size of your monitor.
Explain the mechanics: no window manager, a fixed default window, responsive breakpoints firing, and why a launch argument beats a later resize. Name the argument and where it is set.
Show you can recognise the failure signature in a pipeline log — layout-sensitive tests failing together while data assertions stay green — and fix it once in the driver factory rather than test by test.
Own the viewport as a declared property of a suite: which widths are covered, why, how they are kept identical between local runs and pipelines, and what it costs to add another one.
## What actually changes when the browser goes headless A headed browser gets its size from the desktop it opens on: a window manager places the window, and a call to maximize it fills whatever monitor is there. Headless has none of that. There is no desktop, no window manager, and no monitor, so the browser opens at a **fixed default window size** of its own choosing rather than at the size of anything around it. For Chrome that default is small — 800 by 600 — which is narrower than nearly every developer's screen. That single fact explains the usual CI surprise. Locally you run headed on a wide monitor and the farm irrigation-schedule panel lays out as a full weekly grid. On the build agent the same suite runs headless at the small default, the panel's responsive breakpoints fire, and the grid collapses into the stacked mobile layout with the zone controls folded behind a menu button. The test that clicks "Run zone 3 now" was never wrong; it is looking at a different rendering of the same page. ## Why resizing afterwards is the wrong instinct The reflex is to open the session and then ask the browser to maximize. On a build agent that is weak for two reasons. - **There is nothing to maximize into.** The W3C WebDriver specification defines maximizing in terms of the operating system's window manager, and states that because window managers differ, not all of these commands can be supported by all remote ends — support is advertised by the `setWindowRect` capability, and an unsupported command returns an `unsupported operation` error. - **It happens too late.** Anything the browser has already loaded was laid out at the old size. You are now relying on a resize event and a re-layout landing before your first assertion, which is a race you introduced yourself. Setting the size at **launch** avoids both. `--window-size=1600,1200` is a browser argument, applied before the first paint, so the very first page load already has the width the suite expects. ```java ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new", "--window-size=1600,1200"); WebDriver driver = new ChromeDriver(options); ``` ## Launch argument versus a later resize | | `--window-size` at launch | Resizing after the session opens | |---|---|---| | When it applies | before the first paint | after at least one layout has happened | | Depends on a window manager | no | maximizing does | | Reproducible across agents | yes, it is a literal number | depends on the host | | Interacts with early assertions | no race | a re-layout race you must wait out | ## What this looks like in a real suite Symptoms cluster, and once you recognise the cluster you stop debugging individual tests: - Elements that exist in the DOM are reported as not displayed, because the narrow layout hid them. - A click lands on the wrong control, because the responsive layout moved a neighbour under the pointer. - Tests that pass one at a time locally fail as a block in CI, since every one of them shares the same wrong viewport. - Only the layout-sensitive tests fail; API-shaped assertions in the same suite stay green, which is the tell that the page is fine and the window is not. ## Making the viewport an input, not an accident 1. **Choose the width the suite is asserting about** and write it down. If the irrigation panel's weekly grid needs 1200 CSS pixels, the suite's window must be wider than 1200 plus whatever chrome the browser reserves. 2. **Set it at launch, in one place** — the same factory that builds the driver — so no individual test can drift. 3. **Use the same number locally.** Running headless locally with the CI window size is the cheapest way to reproduce a CI-only layout failure, and it removes the "works on my machine" argument entirely. 4. **Treat a second width as a second run**, not as a resize in the middle of a test. If the suite genuinely covers a narrow layout, that is a separate configuration with its own launch argument. On Selenium 4 the argument is passed through the browser options object like any other Chromium switch, and it is inherited by every session that factory produces. `--headless=new` selects Chrome's modern headless mode, which is what current Selenium documentation recommends; on recent Chrome builds plain `--headless` now selects that same mode. Neither spelling changes the sizing story: the default is small, and the fix is to state the size you want instead of hoping the environment supplies one.
- Why is setting the size at launch better than resizing right after the session opens?A launch argument applies before the first paint, so every page the session loads has the right width. Resizing afterwards means at least one layout already happened at the wrong size, and you are betting that the re-layout completes before your first assertion. It also depends on window commands the agent may not meaningfully support.
- How would you reproduce a CI-only layout failure on your own machine?Run the same suite headless locally with the identical window-size argument. That reproduces the viewport exactly, which is usually the whole difference. If it still passes locally, the cause is elsewhere — timing, data or network — and you have cheaply ruled out the most common explanation.
- The suite must cover both a wide and a narrow layout. How do you structure that?Run it twice with two different launch sizes rather than resizing mid-test. Each configuration is then internally consistent, screenshots and failures are attributable to one width, and no test carries a hidden resize step whose timing you have to reason about.
saying these in an interview costs you the question
- Believes headless inherits the agent's screen resolution
- Calls maximize on an agent that has no window manager
- Blames flaky waits for a purely layout-driven failure
- Resizes mid-test and asserts before the re-layout lands
- Uses a different viewport locally than the pipeline does