Skip to content

[Regression]: Firefox cannot move focus from an iframe to its parent since r1553 #43280

Open

Description

@diegohaz

Last Good Version

1.63.0. With the Playwright client fixed at 1.64.0, Firefox browser build r1552 is the last good build in the bisect below.

First Bad Version

1.64.0, the latest stable release as of October 11, 2026. Firefox browser build r1553 is the first bad build.

Steps to reproduce

Minimal reproduction: https://github.com/diegohaz/playwright-firefox-iframe-focus-repro

git clone https://github.com/diegohaz/playwright-firefox-iframe-focus-repro.git
cd playwright-firefox-iframe-focus-repro
npm ci
npx playwright install firefox
npm test
npm test -- --headed

The repository has one test file, plain HTML, and only @playwright/test as a dependency. Each test gets a fresh page. It checks three ways to move focus from an iframe back to its parent:

  1. Click the iframe input, then press Tab to reach the next parent input.
  2. In a button's click handler, focus the iframe input, then call parentInput.focus({ preventScroll: true }) in a microtask.
  3. Repeat the second case with iframe.contentWindow.focus() before focusing the iframe input.

Expected behavior

The parent input receives focus, and expect(page.locator('#outer')).toBeFocused() passes in all three tests, in both headed and headless mode.

Actual behavior

All three tests fail in both modes with Playwright 1.64.0's bundled Firefox r1555 (Firefox 157.0):

Error: expect(locator).toBeFocused() failed
Locator: locator('#outer')
Expected: focused
Received: inactive

In the original diagnostic fixture, the parent document's activeElement remains the iframe instead of the parent input. The failure also occurs with native DOM .focus() calls; it does not require Playwright keyboard input.

This surfaced while updating Ariakit's browser tests in ariakit/ariakit#7847. It prevents tests from exercising focus restoration and keyboard navigation after focus enters an iframe. The reproduction does not use Ariakit or any other UI library.

Additional context

I bisected the official macOS arm64 Firefox archives while keeping the Playwright client at 1.64.0 and changing only executablePath:

Firefox build Firefox version Headless Headed
r1551 156.0 3 pass 3 pass
r1552 156.0 3 pass 3 pass
r1553 156.0 3 fail 3 fail
r1555 (bundled with 1.64.0) 157.0 3 fail 3 fail

Last good: r1552. First bad: r1553. #42958 rolled Firefox from r1551 to r1553 and changed its focus-emulation patches. All three builds around the boundary use Firefox 156.0, so this boundary does not coincide with a Firefox version change.

The reproduction README includes the archive URLs and commands to repeat the bisect. The original per-scenario results and archive SHA-256 checksums are also included. The smaller test file published in the repository was checked separately against all four builds in both modes, with retries disabled.

Playwright 1.63.0's bundled Firefox r1543 (Firefox 155.0) passed all three original scenarios in both modes. A manual plain-HTML test with the same focus sequences also passed in ordinary Firefox 157.0.1 on the release channel, on this Mac. Updating the bundled Firefox version from 156 to 157 did not remove the regression.

The focus-emulation changes in #42958 are a likely cause. In particular, ShouldOverrideHasFocus() now inherits the root docshell's override, which is also used by the existing early return in nsFocusManager::BlurImpl when leaving a document. This is a source-level hypothesis; I have not confirmed the exact C++ change with a revert and rebuild. Related context: #42933 and its regression test in #42969.

Environment

The output of npx envinfo --preset playwright is below, with home-directory paths shortened to ~. The tests used Node.js 24.21.0.

System:
  OS: macOS 26.6.2
  CPU: (14) arm64 Apple M4 Pro
  Memory: 2.97 GB / 48.00 GB
Binaries:
  Node: 24.21.0 - ~/.codex/worktrees/a9b9/ariakit/node_modules/.pnpm/node@runtime+24.21.0/node_modules/node/bin/node
  npm: 12.0.1 - ~/Library/pnpm/bin/npm
  pnpm: 12.3.0 - ~/Library/pnpm/bin/pnpm
  bun: 1.0.0 - ~/.bun/bin/bun
IDEs:
  VSCode: 1.137.0 - /usr/local/bin/code
  Cursor: 3.20.17 - /usr/local/bin/cursor
  Claude Code: 2.1.296 - ~/.local/bin/claude
  Codex: 0.160.0 - ~/.local/bin/codex
Languages:
  Bash: 3.2.57 - /bin/bash
npmPackages:
  @playwright/test: 1.64.0 => 1.64.0

Activity

  1. pavelfeldman commented on Oct 12, 2026

    @pavelfeldman
    Member

    Hi, the Playwright bot looked into this on my behalf and reproduced it locally. Thanks for the bisect, it points at the right change.

    Reproduced on tip-of-tree (sha d9f2fd3, Firefox r1559), Firefox only, headless and headed. All three of your scenarios fail: Tab from the iframe input, a programmatic focus() after focusing the iframe input, and the same from a click handler. Chromium passes all three.

    The cause is in our Firefox focus-emulation patch, as you suspected. nsFocusManager::BlurImpl returns early, without blurring, when a document is being left and ShouldOverrideHasFocus() is true; that's how we keep the page focused when another page takes focus. In r1553, ShouldOverrideHasFocus() started returning the root docshell's override (for #42933, so iframes inside an emulated-focused page report focus). Moving focus from an iframe to its parent also "leaves" the iframe's document, so for any frame the early return now fires and focus stays in the iframe. The fix belongs in the Firefox patches: the BlurImpl early return should only apply when focus leaves the page, not when it moves between frames of the same page. #42969's test has to keep passing alongside it.

    Minimal repro
    it('should move focus from an iframe to its parent', async ({ page, server }) => {
      server.setRoute('/frame.html', (req, res) => {
        res.setHeader('content-type', 'text/html');
        res.end(`<input id=inner>`);
      });
      await page.goto(server.EMPTY_PAGE);
      await page.setContent(`<iframe src="${server.PREFIX}/frame.html"></iframe><input id=outer>`);
      await page.frameLocator('iframe').locator('#inner').focus();
      await page.evaluate(() => (document.getElementById('outer') as HTMLInputElement).focus());
      await expect(page.locator('#outer')).toBeFocused();
    });
    What the bot ran
    • tip-of-tree d9f2fd3e2, Linux; Firefox r1559 (157.0), Chromium 156, WebKit 27.2
    • Firefox: 3/3 fail, headless and headed
    • Chromium: 3/3 pass
    • WebKit: programmatic focus cases pass; the Tab case does not reach #outer either, which looks like a separate WebKit difference and wasn't investigated further
    • source: browser_patches/firefox/patches/bootstrap.diff, nsDocShell::ShouldOverrideHasFocus() now returns GetRootDocShell()->mOverrideHasFocus (changed in feat(firefox): roll to r1553 #42958), used by the early return in nsFocusManager::BlurImpl when aIsLeavingDocument is set

    Investigated locally by the Playwright bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions