Skip to content

perf: render command log in an isolated iframe to prevent renderer crash#34242

Open
mschile wants to merge 25 commits into
developfrom
mschile/busy-poincare-345146
Open

perf: render command log in an isolated iframe to prevent renderer crash#34242
mschile wants to merge 25 commits into
developfrom
mschile/busy-poincare-345146

Conversation

@mschile

@mschile mschile commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Additional details

Why was this change necessary?

When the application under test drives heavy layout work through a ResizeObserver loop (e.g. an Element Plus dialog dynamically showing ~10+ form fields with an open dropdown popper), the Chromium renderer process can crash (We detected that the Chrome Renderer process just crashed) — but only under Cypress. The crash is a Blink layout-phase fault that occurs when the live, re-rendering React/MobX command-log tree is laid out in the same document as the AUT's heavy reflow. It happens at the engine level, before JS error handling, so Cypress.on('uncaught:exception') cannot intercept it.

Weaker isolation was verified insufficient during investigation: Shadow DOM still crashed (3/3), contain: strict still crashed (3/3), and visibility: hidden/opacity: 0 still crashed — only rendering the command log into its own document prevents the crash (0/4 crashes at the reproduction threshold, versus 4/4 inline). A standalone (non-Cypress) reproduction for an upstream Chromium report was attempted and is not currently feasible; the crash requires the integrated runner environment.

What is affected by this change?

The command log now renders into a same-origin iframe (#reporter-frame) instead of inline in the app document. The reporter's JS still runs in the top window — the reporterBus EventEmitter and MobX stores are passed by reference across the frame boundary — but its DOM, layout, and paint live in a separate document.

Because the reporter's code executes in the top window while its DOM lives in the iframe, code that binds document-level listeners or portals DOM nodes now targets the reporter's document (tracked via setReporterDocument):

  • keyboard shortcuts bind to both the top and reporter documents
  • tooltips (@cypress/react-tooltip) append to the reporter's body via a wrapper, so their coordinates and document agree
  • the runnable options popover portals into the reporter's document, and clamps its position/width so it remains visible in narrow panels (it can no longer overhang the panel the way it could inline)
  • all parent stylesheets and root classes are cloned into the iframe (reporter layout relies on app-level resets like the Tailwind preflight box-sizing rule), with a targeted override for Tailwind's responsive .container component, which collides with the reporter's .container element — inside the iframe its media queries resolve against the frame width and would otherwise clamp the command log at Tailwind breakpoints
  • the React tree mounts only after the cloned stylesheets load, so the reporter never lays out (or receives interactions) unstyled
  • panel resizing applies the same pointer-events guard the AUT panel already used, since iframes swallow the mousemove events that drive dragging
  • falls back to inline rendering if the iframe cannot be set up

Test changes

Cypress-in-Cypress specs can no longer reach command-log content with top-document queries. A cy.reporter() helper (packages/app/cypress/e2e/support/reporter.ts) yields the iframe's body once the reporter has mounted, and ~40 spec files were migrated to scope reporter-owned queries through it. The spec loaders also now wait for the new spec's file name in the runnable header before asserting, since in-app spec navigation remounts the iframe and consecutive specs can render identical test titles.


Note

Medium Risk
Changes how the interactive command log is mounted and how document-level UI (shortcuts, tooltips, popovers) binds, which is user-visible runner infrastructure; mitigated by inline fallback and broad cy-in-cy test updates.

Overview
Fixes Chromium renderer crashes when heavy AUT layout (e.g. ResizeObserver loops with dialogs and many fields) runs while the command log is visible, by hosting the reporter in a same-origin iframe (#reporter-frame) so its layout is no longer in the same document as the AUT panel.

Runner / reporter: Mounts React into the iframe after cloning parent stylesheets and root classes, with a Tailwind .container override and a short delay before showing the frame so it is not interactive unstyled. Cleans up the frame on unmount and falls back to inline rendering if setup fails. Exposes setReporterDocument so shortcuts, tooltips, and the runnable options popover target the iframe document (including popover width/position clamping in narrow panels). Reporter panel resizing disables pointer events on panel content while dragging so iframe hit-testing does not block resize.

Automation: BiDi AUT context selection uses window.name (AUT_FRAME_NAME_IDENTIFIER) instead of assuming the first child iframe, now that the reporter iframe is also a direct child of the top context.

Tests / tooling: Adds cy.reporter() for Cypress-in-Cypress specs and migrates reporter assertions across the app e2e suite; spec loaders wait for the runnable header file name after navigation. Driver command-log helpers and Percy overrides query the reporter iframe document when present.

Changelog documents the performance fix for #33962 and #34218.

Reviewed by Cursor Bugbot for commit 0be5e19. Bugbot is set up for automated code reviews on this repo. Configure here.

Steps to test

  1. Create a Vue project using the reproduction from #33962 (Element Plus dialog with count = 9 dynamic form fields and a filterable select).
  2. Run the spec in Chrome (cypress run or cypress open). Before this change, on affected machines the renderer crashes when the select is filtered; with this change the spec passes.
  3. In open mode, exercise the command log: hover/pin snapshots, collapse/expand tests, tooltips, the runnable options popover (including with a narrow reporter panel), keyboard shortcuts (r, f, a) with focus inside the command log, and drag-resizing the reporter panel across a range of widths.

How has the user experience changed?

No intended visual or behavioral change — the command log looks and behaves as before; it is now hosted in an isolated document. Verified visually and via scripted interaction tests (snapshot hover/pin, popover placement, tooltip placement, panel resizing, keyboard shortcuts).

PR Tasks

  • Is there an associated issue with maintainer approval for PR submission?
  • Have tests been added/updated?
  • [na] Has a PR for user-facing changes been opened in cypress-documentation?
  • [na] Have API changes been updated in the type definitions?

mschile and others added 6 commits July 7, 2026 15:45
The Chromium renderer process could crash ('We detected that the Chrome
Renderer process just crashed') when the application under test performed
heavy layout work driven by a ResizeObserver loop (e.g. an Element Plus
dialog dynamically showing ~10+ form fields with an open dropdown popper)
while the command log was visible.

The crash is a Blink layout-phase fault that occurs when the live,
re-rendering React/MobX command-log tree is laid out in the same document
as the AUT's heavy reflow. Weaker isolation was verified insufficient:
Shadow DOM and CSS containment (contain: strict) still crash, as do
visibility/opacity tricks. Only moving the reporter into its own document
(a same-origin iframe) prevents the crash, while the reporterBus
EventEmitter and MobX store keep working across frames by reference.

Because the reporter's JS still runs in the top window, code that binds
document-level listeners or portals DOM nodes now targets the document
the reporter renders into (tracked via setReporterDocument):

- keyboard shortcuts bind to both the top and reporter documents
- tooltips append to the reporter's body instead of the top body
- the runnable options popover portals and binds click-outside/scroll
  listeners in the reporter's document
- all parent stylesheets (not just cypress_runner.css) and root classes
  are cloned into the iframe, since reporter layout relies on app-level
  resets like the Tailwind preflight box-sizing rule

Falls back to inline rendering if the iframe cannot be set up.

Fixes #33962
Fixes #34218

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reporter now renders inside a same-origin iframe (#reporter-frame),
so cypress-in-cypress specs can no longer reach command-log content with
top-document queries.

Adds a cy.reporter() helper that yields the iframe's body once the
reporter has mounted (the body rather than the .reporter root, so
portaled UI like tooltips and popovers stays reachable), and migrates
reporter-owned queries across the runner, studio, subscriptions, and
specs-list suites. Top-document app chrome, AUT-iframe, and
cloud-studio-bundle selectors are intentionally left unscoped.

Also hardens the spec loaders against the reporter remount that happens
on in-app spec navigation: loadSpec/runSpec now wait for the new spec's
file name in the runnable header so queries can't anchor to the outgoing
spec's reporter (two specs can render identical test titles), and the
reporter defers its React mount until the iframe's cloned stylesheets
load so clicks never land mid-layout-shift.

The clear-all-sessions test now proves the rerun via the session tags
flipping to 'created' instead of asserting the transient loading state,
and the origin-communicator spec toggles the spec list with a click —
cy.type() resolves focus against the AUT document, which doesn't work
for elements in a nested iframe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Panel resizing tracks mousemove on the resizable-panels root, but iframes
swallow mouse events, so dragging the reporter panel's handle stalled as
soon as the cursor crossed the command-log iframe. Apply the same
pointer-events guard the AUT panel already uses: the panel2 slot now
exposes isDragging (true while any panel drags, since panel 1's handle
also crosses the reporter) and both runner modes disable pointer events
on the reporter content during a drag. The guard is scoped to the slot
content rather than the panel so the resize handle itself stays
interactive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The popover renders in the command-log iframe and can no longer overflow
into the rest of the app the way it could when the reporter was inline,
so a narrow panel clipped it at the iframe's left edge. Clamp its left
position inside the frame and cap its width to the frame's viewport so
it shrinks instead of hiding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app stylesheets cloned into the command-log iframe include Tailwind's
responsive `.container` component, which collides with the reporter's
`.container` element. Inline this never mattered because the media
queries resolved against the full window width, but inside the iframe
they resolve against the frame's own width, so a panel wider than a
Tailwind breakpoint clamped the command log to that breakpoint's
max-width (640px, 768px, ...) and the content stopped tracking panel
resizes. Override the max-width inside the iframe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mschile mschile changed the title fix: render command log in an isolated iframe to prevent renderer crash perf: render command log in an isolated iframe to prevent renderer crash Jul 8, 2026
@mschile mschile self-assigned this Jul 8, 2026
@mschile mschile marked this pull request as ready for review July 8, 2026 04:29
Comment thread packages/app/src/runner/reporter.ts
Comment thread packages/app/src/runner/reporter.ts
mschile and others added 2 commits July 7, 2026 22:54
Deferring the React mount until the iframe's cloned stylesheets loaded
meant setupReporter marked the reporter initialized before its event
listeners existed — a resetReporter round-trip could wait forever for
reporter:restarted and early driver events could be missed. Mount
synchronously and hide the iframe until the stylesheets load instead;
hidden elements fail actionability, which equally protects interactions
from landing mid-layout-shift.

Also track the iframe as soon as it is appended so the inline fallback
removes it if the remaining setup throws, and relax the event-manager
reset assertions to tolerate the delayed file-watcher rerun that
re-scaffolding the fixture project can trigger (it runs the same
teardown path, making exactly-once flaky).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/app/src/runner/reporter.ts
Comment thread packages/app/cypress/e2e/runner/sessions.ui.cy.ts Outdated
Reveal the command log after a short timeout even if a cloned stylesheet
never fires load/error, so a hung request cannot leave the reporter
invisible.

Also scope the sessions.ui expander clicks to the first session command
row. The pre-migration chained `.get()` reset to the document root, so
the migration had preserved a click on the reporter's first expander;
targeting the expander inside the session row matches the test's intent
and the pattern used elsewhere in the spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/app/src/runner/reporter.ts
…ering

If reporter setup throws after the React root is created on the iframe
document, the inline fallback previously created a second root while the
first remained mounted on the removed iframe, leaking a detached tree
and its listeners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5c45eb3. Configure here.

Comment thread packages/reporter/src/runnables/runnable-popover-options.tsx Outdated
mschile and others added 3 commits July 7, 2026 23:16
The popover's outside-click and scroll handlers only listened on the
reporter's document, so clicking or scrolling elsewhere in the app (AUT
panel, specs list, sidebar) no longer closed it the way it did when the
reporter rendered inline. Bind the handlers on both the top and reporter
documents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g iframe

The driver suite's command-log helpers and the reporter's special
characters spec queried the runner's reporter through the top document,
which no longer reaches it now that the command log renders in a
same-origin iframe. Resolve the reporter's document through
#reporter-frame with an inline-rendering fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initial migration swept the top-level runner specs but not the
runner/issues/ subdirectory, and left one block in studio-new-tests
unscoped. Point these command-log queries at the reporter iframe:

- issue-9162: the failure message assertion
- issue-18042: the Spies/Stubs instrument toggle and call-count cells
  (clicking the .hook-header directly, since a bare contains from the
  iframe body matched a different element than it did inline)
- studio-new-tests: the recorded new-test row and its commands

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cypress

cypress Bot commented Jul 8, 2026

Copy link
Copy Markdown

cypress    Run #72375

Run Properties:  status check failed Failed #72375  •  git commit 0be5e19d84: test: keep the f shortcut coverage and wait for reporter readiness
Project cypress
Branch Review mschile/busy-poincare-345146
Run status status check failed Failed #72375
Run duration 09m 08s
Commit git commit 0be5e19d84: test: keep the f shortcut coverage and wait for reporter readiness
Committer Matthew Schile
View all properties for this run ↗︎

Test results
Tests that failed  Failures 1
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 1
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 9
View all changes introduced in this branch ↗︎

Warning

No Report: Something went wrong and we could not generate a report for the Application Quality products.


Tests for review

Failed  cypress/e2e/studio/studio-basic.cy.ts • 1 failed test • app-e2e

View Output

Test Artifacts
Cypress Studio - Basic Functionality > removes pending commands when rerunning the test Test Replay Screenshots

mschile and others added 8 commits July 8, 2026 05:06
…frame

System-test project fixtures inspect the command log to verify error
rendering, session tags, and mount logs, and drive the reporter's
stop/restart controls. They queried the reporter through the top
document, which no longer reaches it now that the command log renders in
a same-origin iframe — so the verify/inspection tests failed, adding an
extra failure to each fixture run (error-ui sourcemaps, cy.origin error
stack, svelte mount log, session persistence, runner reload).

Resolve the reporter's document through #reporter-frame with an
inline-rendering fallback in the shared fail/verify helper and the
affected fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BiDi automation assumed the AUT iframe was always the first direct child
browsing context created under the top-level context. With the command log now
rendering in its own iframe, the top-level context has two direct children whose
creation order is not guaranteed, so the reporter frame could be misidentified as
the AUT — breaking cy.origin network interception in Firefox.

Identify the AUT by its window.name (seeded with AUT_FRAME_NAME_IDENTIFIER),
matching how the CDP and WebKit automations locate the AUT frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bidi_automation unit tests emit a single child browsing context and expect
it to be adopted as the AUT. Now that the AUT is identified by its window.name
rather than by creation order, stub scriptEvaluate to return a name carrying
AUT_FRAME_NAME_IDENTIFIER, and add a case asserting a non-AUT child frame (the
reporter iframe) is ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zonejs-mocha-skip/only Angular CT fixtures read the reporter command log via
window.top.document.body to verify skipped/only test titles. With the command log
now in the #reporter-frame iframe, those queries missed — zonejs-mocha-skip failed
its 6 verifications (Angular "with mount tests" exit code 6) and zonejs-mocha-only
would false-pass its not.exist checks against the wrong document. Resolve the
reporter body from the iframe with a fallback to the top document, matching the
other migrated fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Percy snapshot override manager mutates dynamic elements (command progress
bars, spinners, tooltips, active-test animations) to visibility:hidden/frozen
before serialization so snapshots are deterministic. It queried only the top
document, but the command log now renders in the same-origin #reporter-frame
iframe, so those reporter-targeting selectors matched nothing and the dynamic
content leaked into snapshots. Also query and observe the reporter iframe
document so the same overrides apply there and are reverted after the snapshot.
No-op when the reporter renders inline (component tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rendering the command log created its React root directly on the iframe's
<body>. React warns this causes subtle reconciliation issues because body's
children get mutated by other code, and it showed up as unstable reporter
rendering under load (off styling, and the f-shortcut/spec-list toggle not
taking) — flaking cy-in-cy specs on slower CI agents while passing locally.

Mount into a dedicated #unified-reporter container inside the iframe body,
mirroring the inline layout, and give the frame's html/body/container full
height so the reporter fills the panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mschile and others added 3 commits July 13, 2026 13:34
Cloning the app's <link rel="stylesheet"> nodes into the reporter iframe copied
their raw href attributes. The built app references its main CSS with a relative
href (Vite's ./assets/*.css), which resolves against the iframe's about:blank
base and silently fails to load (no request, no 404) — leaving the reporter
without the app stylesheet (Tailwind preflight + design-system tokens) and
visibly unstyled in the built binary/CI while resolving fine under the dev
server. Assign each clone the resolved absolute URL so it loads regardless of
the iframe's base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With the command log now in the #reporter-frame iframe, opening the specs list
with cy.get('body').type('f') (a synthetic keydown to the app body) no longer
reaches the reporter's shortcut handler in the cy-in-cy harness, so the panel
never opened and these two specs failed on Windows CI. Real keystrokes are
unaffected. Click the reporter's specs-list toggle (in the iframe) instead —
the same deterministic approach other cy-in-cy specs already use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the previous commit. The reporter's f keydown handler binds in a
passive effect after it mounts in the #reporter-frame iframe, so pressing f
right after the runnable header appears can race that binding on slower
machines. Rather than route around the shortcut everywhere:

- specs_list_component: restore cy.get('body').type('f') and wait for the run
  to finish first (reporter fully mounted), keeping the shortcut exercised.
- cypress-in-cypress-e2e: this test switches specs mid-run, so it can't wait
  for the run to finish; keep clicking the reporter toggle (onClick binds at
  render) and note the f shortcut is covered by reporter_header.cy.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants