Context: making App Store screenshots for a Capacitor 8 iOS app (SvelteKit SPA bundle, ios.contentInset: 'never', so the WebView runs under the status bar and the CSS pads with env(safe-area-inset-top)).
Rendering the app bundle in headless Chrome at iPhone 17 Pro size (402x874 CSS px, DPR 3 = 1206x2622, the size ASC requires for the 6.1"/6.3" slot) gives env(safe-area-inset-*) = 0, so the layout lacks the notch and home-indicator padding. Chrome has a CDP command for this:
const page = await browser.newPage();
await page.setViewport({ width: 402, height: 874, deviceScaleFactor: 3, isMobile: true, hasTouch: true });
const cdp = await page.createCDPSession();
await cdp.send('Emulation.setSafeAreaInsetsOverride', { insets: { top: 62, bottom: 34, left: 0, right: 0 } });
await page.goto(appUrl);Verified on chrome-headless-shell 151 via puppeteer-core: getComputedStyle(el).top for top: calc(8px + env(safe-area-inset-top)) reads 70px.
It also caught a real app bug: a position: sticky; top: 8px audio player parked under the Dynamic Island once the page scrolled, because with contentInset 'never' the viewport top is the screen top. Fix: top: calc(8px + env(safe-area-inset-top)) (still 8px on the plain web, where the inset is 0). Grep your app for sticky/fixed top: values that skip the inset.
Other pieces that made the pipeline work: the app bundle served by Vite proxies /api on its own origin, so page.setRequestInterception can answer GET /api/p/{key} from prod's public API (and swallow POSTs, so viewing doesn't write milestones on prod) while the shell stays signed in on dev. That gives real content in the real UI with the tab bar. Export the framed composite as JPEG: ASC rejects screenshots with an alpha channel.