the upstream WebKit fix restores the reported WebSocket handshake headers on macOS 26
remove the platform skip so the existing HAR assertions run again
see <https://github.com/microsoft/playwright/issues/42205>
remote browser servers can receive Playwright Test worker paths and try to write trace files on the client filesystem
remove `artifactsDir` and `tracesDir` from remote launch options
fixes <https://github.com/microsoft/playwright/issues/42394>
Closes#42439
## problem
the filter summary under the search input in UI Mode toggles the status and project filters when you click it, but it is a `div` with an `onClick`:
```tsx
<div className='filter-summary' title={...} onClick={() => setExpanded(!expanded)}>
```
so tab skips it, enter and space do nothing, and a screen reader announces it as ordinary text rather than a control that expands something. the chevron next to it is already a button, which makes the summary the odd one out rather than a deliberate choice.
## fix
make it a real `button` with `aria-expanded`.
using a native button rather than `role='button'` plus `tabIndex` plus a keydown handler means focus, enter, space and the button role all come from the platform. it also matches `Expandable` right above it, which already renders its toggle as a `button` with `aria-expanded`.
the css resets the button chrome the same way `.expandable-title-button` does and adds the same focus ring, so it looks the way it did:
```css
.filter-summary {
all: unset;
display: block;
/* ...the previous properties... */
}
.filter-summary:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
```
one thing worth calling out: the original properties lived in a rule shared with `.filter-title`, so the reset is in its own `.filter-summary` block rather than added to the shared one. `.filter-title` is untouched.
## test
`should toggle filters from the keyboard` in `ui-mode-test-filters.spec.ts`, close to your snippet in the issue, plus the aria state and the space key:
```ts
await expect(summary).toHaveRole('button');
await expect(summary).toHaveAttribute('aria-expanded', 'false');
await summary.focus();
await expect(summary).toBeFocused();
await summary.press('Enter');
await expect(page.getByTestId('status-filters')).toBeVisible();
await expect(summary).toHaveAttribute('aria-expanded', 'true');
await summary.press('Space');
await expect(page.getByTestId('status-filters')).toBeHidden();
```
on `main` it fails at the first assertion, `toHaveRole('button')`.
## verification
| check | result |
|---|---|
| new test on `main`, rebuilt | fails |
| new test with the change | passes |
| `ui-mode-test-filters` | 12 passed |
| every `ui-mode` spec | 139 passed |
| eslint on both changed files | clean |
---
disclaimer: this contribution was prepared with the assistance of an ai agent. i reproduced the behaviour, checked how `Expandable` solves the same problem before picking an approach, and ran the ui mode suites locally before opening this.
Fixes#42363
### What happens
With the HTTP transport and `--shared-browser-context`, `browser_close` disposes the calling client's `BrowserBackend`: the `Context` is cleared and the dispose callback only decrements the client count, because another client keeps the shared browser alive. `server.ts` caches `backendPromise` per session and clears it only on the backend's `disconnected` event, which `BrowserBackend` emits only when the browser context closes or the browser disconnects. Neither happens here, so the next tool call from that client reuses the disposed backend: `ensureBrowserContext` returns the already resolved promise without re registering the page listener, `newTab` finds no tab, and `browser_tabs` (new) or `browser_navigate` fail with
```
TypeError: Cannot read properties of undefined (reading 'checkUrlAndNavigate')
TypeError: Cannot read properties of undefined (reading 'waitForInitialized')
```
until the server restarts. In the CLI server, non shared and isolated modes are unaffected because they close the context or the browser and the event fires.
### The change
`BrowserBackend.dispose()` now emits `disconnected` (guarded so it fires once) after the context and the dispose callback have run. `server.ts` already drops the cached backend in its existing `once('disconnected')` listener, so the next call creates a fresh backend through the factory. The listener also calls `backend.dispose()` again, which is a no op through the existing `_disposed` guard.
This was chosen over special casing the close result in `server.ts`: `callTool` already strips `isClose` from the result before returning it, so the server cannot see a close without widening the `ServerBackend` contract, and the event approach makes every disposal path invalidate the cache, not only `browser_close`. No debug line is logged from `dispose`, so the tests that count `browser disconnected` lines are unaffected.
The library entry point (`createConnection` in `mcp/index.ts`) goes through the same `createServer`, so it picks up the same behaviour: after `browser_close` the next call now drops the disposed backend and asks the factory for a new one (a fresh `BrowserBackend` on the context the getter returns when a `contextGetter` is supplied), instead of failing with the TypeError. Without a getter the factory launches a second browser while the first stays open, because `createConnection` passes no dispose callback and `browser_close` never closed the browser in library mode before or after this change; that is pre existing. The other `BrowserBackend` consumers (`cli-daemon`, `traceSnapshot`, the test backend under `playwright/src/mcp`) register no `disconnected` listener, and an emit with no listener is harmless.
### Test
New `http transport shared context survives browser_close` in `tests/mcp/http.spec.ts`, modelled on the neighbouring shared context test: two HTTP clients with `--shared-browser-context`, both navigate, client 1 calls `browser_close` then `browser_tabs` (new) and must get a snapshot, client 2's `browser_snapshot` must still work, and the log counts must match (`create context 3`, `close browser 1`).
```
# chromium project, tests/mcp/http.spec.ts
with the change: 20 passed, 1 failed
source reverted, test kept: the new test fails with TypeError ... 'checkUrlAndNavigate' at the browser_tabs assertion
# chromium project, tests/mcp/launch.spec.ts and cdp.spec.ts (the tests that count browser disconnected lines)
with the change: 19 passed
```
The one failure in `http.spec.ts` is `should close session when heartbeat ping is not answered`, and it fails identically with the source reverted, so it is unrelated to this change: on the machine used here the default `chrome` channel takes 7 to 8 seconds to launch a fresh persistent profile, and the heartbeat only starts once the backend exists, so the session delete lands after `expect.poll`'s default 5 second timeout. With `--browser=chromium` the same scenario reaps the session in under 3 seconds.
The issue's three step scenario was also run end to end against a built `mcp.js` with two streamable HTTP clients before and after the change; before, step 3 returns the TypeError above and the second client keeps working; after, all steps succeed.
`eslint`, `tsc`, `lint-tests` and `check-deps` pass. Only the chromium project was run locally; chrome, firefox and webkit were not.