I was closing out a milestone the other day and ran the whole verification list from the plan: lint, contracts, unit tests, database integration, race tests, build, browser journeys. Everything went green in under a minute except the browser journeys, which failed ten out of ten in three seconds.

Error: browserType.launch: Executable doesn't exist at
/Users/dusan/Library/Caches/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-mac-arm64/chrome-headless-shell
╔════════════════════════════════════════════════════════════╗
║ Looks like Playwright was just installed or updated.       ║
║ Please run the following command to download new browsers: ║
║                                                            ║
║     pnpm exec playwright install                           ║
║                                                            ║
║ <3 Playwright Team                                         ║
╚════════════════════════════════════════════════════════════╝

Fair enough. Playwright pins browser builds per version, and the cache had nothing for this one. The CI workflow installed Chromium only, so I did the same thing rather than pulling all three engines and ffmpeg:

pnpm --filter @webkeeper/web exec playwright install chromium

Fifty-five seconds later, Chrome for Testing 151.0.7922.34 was in the cache. I ran the suite again.

Five passed. Five failed. Same error box, different path:

Error: browserType.launch: Executable doesn't exist at
/Users/dusan/Library/Caches/ms-playwright/webkit-2336/pw_run.sh

WebKit. I hadn't asked for WebKit. I opened the config to prove it.

The config that never says WebKit

projects: [
  { name: 'desktop', use: { ...devices['Desktop Chrome'] } },
  { name: 'mobile', use: { ...devices['iPhone 13'] } },
],

Nothing about engines. Just two device names, which is the whole point of the devices table: pick a phone, get a sensible viewport, scale factor, user agent and touch support without typing them out.

Except the descriptor carries one more field. Here's what devices['iPhone 13'] actually expands to in Playwright 1.62.1:

{
  "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Mobile/15E148 Safari/604.1",
  "viewport": { "width": 390, "height": 664 },
  "screen": { "width": 390, "height": 844 },
  "deviceScaleFactor": 3,
  "isMobile": true,
  "hasTouch": true,
  "defaultBrowserType": "webkit"
}

That last line is the engine choice. It makes sense: an iPhone runs Safari, Safari is WebKit, and if you want to know how your page renders on an iPhone you'd better render it with WebKit. The spread operator copies it into the project along with everything else, and the project launches WebKit without the word appearing anywhere in your file.

I counted. Of the 207 descriptors shipped with this version, 105 default to WebKit, 100 to Chromium and 2 to Firefox. Every iPhone and iPad is WebKit. The Galaxy phones and Pixels are Chromium. So "mobile" in a Playwright config is a coin flip on which engine you've just made a hard dependency, decided by the brand of phone you picked.

It had already broken CI

The part that stung: this wasn't a local-only problem. The GitHub workflow had this step, copied from a template at some point:

- name: Install browser
  run: pnpm --filter @webkeeper/web exec playwright install --with-deps chromium

The browser-journeys job had been red on the last two commits with the exact same webkit-2336/pw_run.sh error. By the time I'd worked it out locally, the fix had already landed on main from the other direction. Same diagnosis, two people, one missing browser.

The fix is one word:

- name: Install browsers
  run: pnpm --filter @webkeeper/web exec playwright install --with-deps chromium webkit

WebKit 26.5 downloaded in fifteen seconds. Ten out of ten, both projects, seventeen seconds end to end.

The other fix, and why I didn't take it

If you only wanted the phone-shaped viewport and touch events, you can keep Chromium and override the engine:

{ name: 'mobile', use: { ...devices['iPhone 13'], defaultBrowserType: 'chromium' } },

That's a legitimate choice for a layout smoke test. It is not a test of how the page behaves on an iPhone, because none of the rendering, form controls or date inputs are Safari's. Since the mobile project exists precisely to catch the things Safari does differently, I'd rather pay fifteen seconds and 100MB of download than pretend.

Takeaway

A device name in a Playwright config is a browser install requirement in disguise. If the error box names an engine you never typed, don't argue with it: print the descriptor (node -e "console.log(require('@playwright/test').devices['iPhone 13'])") and read the last line. Then install what it says, in the same place CI does. A red browser job that names a path is not flaky, it's telling you the filename.