Skip to content

build: release 0.3.0 - #3

Merged
jerrychan7 merged 184 commits into
mainfrom
release/0.3.0
Nov 4, 2026
Merged

jerrychan7 merged 184 commits into
mainfrom
release/0.3.0

Conversation

@jerrychan7

Copy link
Copy Markdown
Owner

Release 0.3.0, based on Chrome 156 (156.0.8078.80).

  • Bumps every package to 0.3.0 and moves the Unreleased changelog entries under ## 0.3.0 (both languages); node scripts/release.ts check 0.3.0 passes.
  • Contents: same-origin, cross-origin and sandboxed iframes, plus the fixes from the review rounds on dev/0.3.0; see CHANGELOG.md.

Verified: full test suite, typecheck and lint pass locally on dev/0.3.0 (the last commits only touch docs). Not checked by hand in Safari or on real devices.

A daily workflow (and on demand) asks Chromium Dash which devtools-frontend
commit stable Chrome ships, as build.ts --latest does, and compares it with
REVISION. When they differ, minor Chrome updates included, it opens an issue
with the update command, once per version, and closes an older one not done
yet. It builds nothing; a Chromium Dash answer without a version or a commit
fails the run.

DEV.md: the frontend follows every stable update, not once per milestone.
…ase/x.y.0

main now holds what is released. Work for the next minor goes on dev/x.y.0, pushed freely, and reaches main through release/x.y.0 (dev -> release -> main); a fix that cannot wait still goes from main to main as a patch, and main is then merged into the dev branch. CI also runs on pushes to dev/**, so that the development branch is checked without a pull request.
With one developer, work in progress on dev/x.y.0 need not queue a run on every push: the release/x.y.0 pull request checks it all, and CI can be run on the branch by hand (gh workflow run ci.yml --ref dev/0.3.0). DEV.md says so.
A roadmap in both languages, linked from DEV.md, for the directions
discussed so far: the missing accessibility tree (what dom-accessibility-api
covers and what is left), real CDP on Android through ADB wireless debugging,
the gaps in Emulation (no setDeviceMetricsOverride, no-ops that succeed),
patches from an older release line, and what is shelved (dragging the
divider in shell mode, touch screens).
iframes: what the agent assumes now (one frame, one execution context),
Chrome's model (same-origin iframes as more frames and contexts in the
target, cross-origin ones as child targets), what each domain needs, the
realm problem (about 166 instanceof checks and 180 uses of the global
document or window), and a step 0 that changes no behavior but keeps new
code from adding to it.

What else can be done in the page and match Chrome, by value for effort:
animations, vision deficiencies, network blocking and offline, the
browser's default styles, layout overlays, emulated media, Application
panel updates, service workers, violations.
…the top realm

The first step towards same-origin iframes (docs/roadmap.md, "iframes"), changing no behavior.

- core/frames.ts: a FrameRegistry, looked up by id, execution context, window or node. Only
  the top document is registered for now. Domains take frame, loader and execution context ids
  from Context.frames instead of the FRAME_ID, LOADER_ID and EXECUTION_CONTEXT_ID constants;
  DOM.describeNode, DOM.getNodeForLocation and style sheet headers use the frame of their node.
- core/realm.ts: node checks by nodeType (isElement, isDocument, isShadowRoot, ...) and a
  node's own document and window (documentOf, realmOf), which hold for an iframe's nodes too.
- scripts/check-realms.ts, in `pnpm lint`: counts per file the `instanceof Element`-like checks
  and uses of the global document, window and getComputedStyle in the agent, against
  scripts/check-realms.json. A file may have fewer, not more.
FrameRegistry now finds the page's same-origin iframes (and <frame>s) as
child frames, each with its own id, loader id, execution context id,
parent, owner element, window and current document. Cross-origin iframes,
whose contentDocument is out of reach, are not registered.

- sync() walks the documents and registers, updates or unregisters frames.
  Like Chrome, a frame keeps its id when it loads another document but gets
  a new loader id and execution context id; a moved iframe gets a new
  browsing context, so it becomes a new frame.
- onChange(listener) reports 'attached', 'navigated' and 'detached'. While
  anyone listens, the registry watches each frame document for iframes
  being added or removed and for their load events, and syncs by itself.
- ofOwner, ofDocument, childrenOf and iteration in tree order serve the
  DOM domain now and Page/Runtime later.

realm.ts gains isHtml(node, name), isNode (a node of any realm, through
the top page's Node.prototype.nodeType getter, which Web IDL accepts across
realms) and isFrameOwner.
An <iframe> or <frame> of a registered frame is serialized with its
frameId and contentDocument, and the nodes in it work with the DOM
commands: getDocument, requestChildNodes, describeNode, resolveNode,
querySelector(All), getOuterHTML, performSearch and
pushNodeByPathToFrontend.

As checked against Chrome:
- contentDocument is not one of the iframe's children: childNodeCount and
  children are the iframe's fallback content, and the document has no
  parentId.
- The document's children follow only with pierce, at the iframe's own
  depth (like shadow roots): describeNode(iframe, depth 2, pierce) lists
  the document with its <html>.
- The document element of each frame carries the frame's frameId.
- Pushing a node inside an iframe sends setChildNodes for the iframe (its
  fallback content), then for its document, and down; the iframe document's
  parent in the path is the iframe.
- performSearch goes through the documents of all frames, the top one
  first, for each step (text, then XPath, then selectors).
- The frontend's node path names an iframe document `d`.

The node checks these need use core/realm.ts instead of the top page's
instanceof (resolveNode accepts a node of any realm; nodes.ts and
outerHTML check node types), lowering their counts in check-realms.json.
Cross-origin iframes stay as they were, without frameId.
The DOM domain's observer also observes the document of each iframe it
sends (one observer for all documents, as for shadow roots, so that the
order of changes across documents is kept), and listens to the frame
registry while the client follows the tree.

As checked against Chrome:
- Attribute, text and child changes inside an iframe document are reported
  like any others.
- An inserted iframe is first sent without frameId and contentDocument
  (Chrome reports it before its frame exists), then removed and inserted
  again with its document: right away for one without src (or about:blank),
  at its load for one that loads a src or srcdoc. The same holds for a moved
  iframe, which gets a new browsing context.
- An iframe that loads another document (src or srcdoc changed, or its own
  location.reload()) is removed and inserted again with the new document;
  the old document's node ids become invalid.
- A removed iframe takes its document's node ids with it.

Chrome sends the new document as it commits, empty, and then counts its
children with DOM.childNodeCountUpdated as they are parsed; ours is sent at
the iframe's load, complete. The client ends up with the same tree.

Frame changes are sent in a microtask after pending mutations and only if
the document differs from the one sent, since an about:blank iframe loads
synchronously while being inserted, before its mutation is reported.
…etOuterHTML

The rest of the DOM domain and core/history.ts (DOM.setOuterHTML and
undo) check node types and use the node's own document and window
instead of the top page's instanceof, document and getComputedStyle, so
that nodes of same-origin iframes work too:

- setOuterHTML parses in the node's own document and, for a document
  element, patches that document; it patched the top one before.
- setAttributesAsText and setNodeName create their nodes in the
  element's document; getContentQuads of a text node uses its document's
  range; box models and getNodesForSubtreeByStyle use the element's
  window's getComputedStyle.
- getNodeForLocation stays on the top document, now said explicitly:
  coordinates inside iframes are for Overlay and geometry to come.

realm.ts gains isSvg, isSvgElement and isMathMlElement. dom.ts no longer
uses the top realm and history.ts once (a string); check-realms.json is
lowered. A conformance test edits nodes of an iframe document.
The layout mapping read computed styles through the top window's
getComputedStyle, checked elements with instanceof HTMLElement / SVG*Element
and took the document, its scrolling element and scrollX/scrollY from the
top page. For an element of a same-origin iframe all of these are wrong:
instanceof fails across realms, so every iframe element looked like
"not HTML" and fell back to the bounding rectangle, and the root elements
were compared with the top document.

The checks now go through core/realm.ts (isHtmlElement, isSvg) and the
element's own window and document; SVGGraphicsElement and
SVGGeometryElement are recognised by their methods, replaced elements by
their local name. Behaviour in the top document is unchanged.
The rules and sheets of a same-origin iframe belong to the iframe's realm:
they are not instances of the top page's CSSStyleRule, CSSMediaRule, ... so
every `instanceof CSS*` in the CSS domain would take an iframe's style rule
for something else and skip it.

core/realm.ts gets isCssom(value, name), which walks the prototype chain
for an interface prototype whose Symbol.toStringTag is `name`. That covers
inheritance too (a CSSMediaRule is a CSSGroupingRule), as instanceof did.
The CSS domain and core/adopted.ts use it instead of instanceof; behaviour
for the top page is unchanged.
…k.ts, per window

The fetch, XHR and WebSocket hooks and the Resource Timing observer move out of
probe/index.ts into probe/network.ts, behind hookNetwork(probe, sink, window, listen).
Every global they use (fetch, XMLHttpRequest, WebSocket, Request, Blob, FormData,
ArrayBuffer, DOMException, PerformanceObserver, location) is now read from the window
given, and instanceof checks use that window's constructors, so that the same hooks can
later be installed in a same-origin iframe, whose objects are not instances of the top
page's.

The probe still hooks only the top window, with the same behavior. Its record keeping
(ids, ring buffers, listeners) is passed in as a NetworkSink. Kept apart from the console
and error hooks, which stay in probe/index.ts. Realm uses drop by 4 (check-realms).
…d events

Page.getFrameTree (new) and Page.getResourceTree now list the same-origin
iframes of the frame registry as childFrames, nested like the frames. Each
frame is described like Chrome describes it: parentId, a new loaderId per
document, name from the window's name else the <iframe>'s id, the URL
without its fragment plus urlFragment, securityOrigin "://" for about:blank
and about:srcdoc documents, mimeType from the document, and the secure
context and cross-origin isolation of the frame's own window. The top
page's resources no longer list iframe documents (Resource Timing's
"iframe"/"frame" initiators): Chrome counts them as the frames' own.

Page.enable now follows the registry and sends, measured against Chrome:
- frameAttached (with parentFrameId), then for an iframe that has a src or
  srcdoc frameStartedLoading, and frameNavigated plus frameStoppedLoading
  once its document is there; Chrome reports nothing for the initial
  about:blank such an iframe leaves (core/frames.ts awaitsFirstDocument).
- for an empty iframe, frameNavigated to about:blank before
  frameStartedLoading and frameStoppedLoading, in Chrome's order.
- for a later navigation, frameStartedLoading, frameNavigated,
  frameStoppedLoading.
- frameDetached with reason "remove" for each removed frame, descendants
  first.
Page.disable and the end of the session stop them. The registry notices a
navigation on the iframe's load event, so the events come later than
Chrome's (which reports the commit). Chrome's experimental
frameRequestedNavigation, frameStartedNavigating and
frameSubtreeWillBeDetached are not sent: their loader id and timing are not
known in the page.
… window

Probe.hookNetwork(win, frame) installs the fetch, XHR and WebSocket hooks and the Resource
Timing observer in another window (a same-origin iframe's), recording into the same lists.
Each request record started there gets a FrameTag (frameId, loaderId, documentURL), from
the `frame` callback called when the request starts: like Chrome, a request keeps the
document that made it even if its frame navigates later. WebSocket records are not tagged:
Chrome's webSocketCreated has no frame either.

A window is hooked once per global object, marked with a symbol: when a navigation keeps
the global (the initial about:blank document), calling again only replaces `frame`.

Resource Timing in such a window:
- entries are shifted from the iframe's time origin to the top page's, so timestamps of
  every frame are on one clock;
- fetch() and XHR entries that started before the hooks are now reported (as Fetch / XHR),
  only later ones are left to the hooks. In an iframe these are the requests of its first
  scripts, which run before the agent learns of the new document; Chrome reports them too.

The probe view recognizes Headers of any realm (an iframe's fetch() returns its own), and
passes hookNetwork through. Optional in the Probe type: probes from before 0.3.0 lack it.
The CSS domain only looked at the top document. The sheet registry now
takes the documents of all registered frames (and the shadow roots in
them) as trees with sheets of their own, so CSS.enable announces the
sheets of each same-origin iframe, and the domain watches every frame's
document for <style>/<link> changes, load events and adopted sheets.

- styleSheetAdded carries the frame the sheet belongs to: that of its
  owner node, or for a constructed sheet the realm it was made in (only
  that realm's trees can adopt it), found by realmOfSheet().
- Like Chrome, an inline sheet's sourceURL is its document's base URL:
  for a srcdoc iframe that is the parent's URL, not about:srcdoc, and a
  <base> in the iframe wins (checked against Chrome). Sheets a script
  inserts and constructed ones stay without a URL and not inline.
- The registry's frame changes drive the domain: an attached iframe, or
  one that loads another document, has its document watched and its
  sheets added; the sheets of a document left behind (navigation or a
  removed iframe) are reported removed, as Chrome does.
- core/adopted.ts hooks adoptedStyleSheets on the prototypes of each
  realm whose trees are watched (watchTrees), as an iframe has its own
  Document and ShadowRoot prototypes.
- <style> elements of an iframe's document present when the agent first
  sees it count as the parser's, like those of the top document.

Adds css-iframe.conformance.test.ts with a styled iframe fixture.
…ion context

Runtime.enable used to announce one context, the top page's. It now
announces the default context of every frame in the registry, and while
enabled follows the frames, as Chrome does (measured against it):
- an iframe's context is created once its document is there; none is
  announced for the initial about:blank an iframe with a src or srcdoc is
  about to leave, like Chrome;
- a navigated frame's context is destroyed, then the new document's created;
- a removed frame's context is destroyed (descendants first, following the
  registry);
- on enable, the contexts come newest first: Chrome lists them in that order.
Each context has the frame's id in auxData, isDefault true, the origin of
its document (inherited by about:blank and about:srcdoc, so the parent's,
as in Chrome) and a uniqueId of its own, which executionContextDestroyed
carries too. Runtime.disable and the end of the session stop following.

The bookkeeping lives in domains/runtime/contexts.ts, which also resolves
contextId / uniqueContextId to a frame for the commands that take one.
…s, quads and scrolling

DOM.getBoxModel and DOM.getContentQuads gave the quads of a node in a
same-origin iframe in the iframe's own viewport. Chrome gives them in the
top frame's viewport, so DevTools drew and measured them off by where the
iframe is.

core/geometry gains frameToTop(doc): the mapping from a frame document's
viewport to the top one, composed over every iframe holding it. Each step
is the iframe's content box (border plus padding), placed by its bounding
rectangle, or through localToViewport when the iframe or an ancestor is
transformed, so rotated and scaled iframes map like Chrome's quads. The
iframe's own scrolling is already in the inner viewport coordinates.
Width and height of the box model stay the layout size, like Chrome's.
Text node quads go through the same mapping.

DOM.scrollIntoViewIfNeeded followed scrollIntoView({block: 'nearest'}).
Chrome uses Blink's "center if needed" alignment for each scroller on its
own: no scroll when the node is visible (or covers the visible part), the
nearest edge when partly visible, centred when hidden; then the viewport,
then the documents of the iframes holding it. scrollIntoViewIfNeeded in
core/geometry does that, which also centres in the top document like
Chrome rather than scrolling to the nearest edge.

New frame-geometry conformance test (fixture test-support/
frame-geometry.html) compares box models, content quads and scroll
positions with Chrome's for a plain iframe with border and padding, a
rotated and scaled one, scrolled documents and scrollers, inline and text
nodes, and a nested iframe.
…ocation

DOM.getNodeForLocation only looked at the top document, so a point on an
iframe found the <iframe> element. Chrome's hit testing goes into the
iframe's document and returns the node there, with that frame's id; on
the iframe's border or padding (outside its document) it returns the
iframe itself.

core/geometry's hitTest does the same: the topmost accepted element at a
point, and for a same-origin frame owner, the hit in its document at the
point mapped through the owner's content box (inverse of the mapping
frameToTop uses, so transformed iframes work too), falling back to the
owner where the inner document has nothing.

Measured against Chrome: its point is in the top document, not the top
viewport (scrolling the top page does not move what a point hits, while
scrolling inside an iframe does). The agent took viewport coordinates; it
now subtracts the top window's scroll position first.
…se URL

fetch() and XHR resolve a relative URL against the document's base URL, not its location:
in a srcdoc iframe the location is about:srcdoc, which is no base at all, so the hooks
failed to parse the URL and let every such request through unrecorded. They now use
document.baseURI, which also follows a <base href> like the browser does.

The Resource Timing navigation entry of an about:blank or about:srcdoc document is
skipped: Chrome reports no request for those documents, only for http(s) ones.
…ame and loader

While Network is enabled, the domain follows the frame registry and has the probe hook
the network APIs of each same-origin iframe document: those there at Network.enable,
and each new one as a frame is attached or navigates (a new document has a new global
object, without the hooks). Following stops at Network.disable; hooks stay, like the
top page's.

requestWillBeSent and responseReceived carry the request's own frameId and loaderId,
and requestWillBeSent the documentURL of the document that made it (about:srcdoc for a
srcdoc iframe), as Chrome does. They are taken when the request starts, so a response
that arrives after its frame navigated keeps the old loader. A document's first scripts
run before the registry hears of it (on load): when a request's window shows a document
other than the registered one, the registry is synced first, so the request gets the new
loader rather than the previous document's.

An iframe's document request is reported once, from the iframe's own Resource Timing
navigation entry: frameId of the iframe, a loaderId of the document it loaded,
documentURL its own URL, as in Chrome. The parent's Resource Timing entry for it
(initiatorType iframe/frame, same origin) is dropped; it used to be reported as a
Document of the top frame.

WebSocket payloads from an iframe are its own realm's Blobs and ArrayBuffers: told apart
by their class, not instanceof. getResponseBody and the other requestId commands are
unchanged: records of every frame share the probe's lists.
Compared with Chrome on the same page, ids by how they relate (frame and loader ids
differ between the two sides):
- an iframe's document request: type Document, its own frame and a new loader,
  documentURL its own URL; fetch, XHR and sendBeacon from it carry that frame, loader
  and documentURL, in requestWillBeSent and responseReceived;
- getResponseBody of an iframe's request;
- a navigated iframe keeps its frame and gets a new loader, also for later requests;
- requests of an iframe document's first scripts and its parser (fixture
  test-support/frame-network.html) carry the document's frame and loader;
- a srcdoc iframe: documentURL about:srcdoc, and no request for the about:srcdoc
  document itself;
- a WebSocket opened in an iframe.
Overlay.highlightNode draws from DOM's box model, which now places a node
of an iframe in the top viewport, so the highlight lands where Chrome
draws it; the layer stays in the top document. Scrolling inside an iframe
moves its nodes but its scroll events do not reach the top window: the
highlighter also listens on the windows of the iframes holding the
highlighted element and redraws on the next frame, like it does for the
top page.

Inspect mode listened on the top window only, where input over an iframe
arrives as nothing (it goes to the iframe's window), so hovering or
clicking an iframe picked nothing inside it. Chrome's inspect mode picks
the node under the mouse in same-process frames. The listeners are now
added to the window of every registered frame, and to frames attached or
navigated while inspecting (through FrameRegistry.onChange); the event
target check works across realms (isNode/isElement instead of instanceof
Element).

The new overlay-iframe conformance test checks the drawn quads of a node
in a rotated, scrolled iframe against Chrome's box model before and after
scrolling the iframe, and that a click in inspect mode picks the same
node in the iframe as Chrome's inspect mode, without the page seeing it.
…r own document

A node in a same-origin iframe now gets its styles the way Chrome
computes them: from the rules of its own document, in its own viewport.

- The cascade looks up the candidates of the node's own tree: its shadow
  root, or else its document (no longer always the top one), so the top
  page's rules never match inside an iframe and the other way round.
  The implicit @scope root falls back to the node's own document element.
- Media queries are evaluated in the window of the sheet they are in
  (realmOfSheet, moved to rules.ts): a sheet's media, @import media,
  active @media rules and the `active` flags of mediaList. An iframe
  narrower than the top page matches its own (max-width) rules, as in
  Chrome. CSSMedia.sourceURL is the sheet's source URL rather than the
  top page's location.
- getComputedStyleForNode and getBackgroundColors use the node's window
  getComputedStyle, as do the background walk (color.ts) and container
  queries (core/container-query.ts), whose vw/vh units and rem now come
  from the node's own viewport and root element.
- getMatchedStylesForNode syncs the frame registry when it does not know
  the node's document yet (no DOM listener running).
- source.ts and cascade.ts check node types with core/realm.ts.

Tested against Chrome: matched, inherited and inline styles of nodes in
a src and a srcdoc iframe, @media/@container rules holding only in the
narrow iframe, computed styles, inline styles and background colors.
…frame

Runtime commands that name a context now run in that frame's realm, as in
Chrome; without one, they run in the top page's as before:
- evaluate takes contextId or uniqueContextId; console input runs through
  the frame window's own indirect eval, so its globals, `var`s and
  top-level await are the frame's. Eager evaluation (throwOnSideEffect)
  reads the frame's globals.
- REPL-mode `let`/`const`/`class` are bound per frame document (a new
  document is a new realm), on the frame's window, with the frame's
  ReferenceError/TypeError/SyntaxError.
- the console utilities ($, $$, $x, clear) work on the frame's document.
- callFunctionOn runs in executionContextId / uniqueContextId if given,
  else in the realm of the object it is called on (Chrome's rule): a node's
  frame, a frame's window, or the frame whose Object.prototype is in the
  object's prototype chain.
- compileScript and globalLexicalScopeNames take executionContextId; the
  lexical names of an iframe come from the scripts of its document.
- exceptionDetails carry the frame's executionContextId, and errors of the
  frame's realm are recognised (stack cleanup, "Uncaught (in promise)
  Error: …").
- an unknown or gone context is answered with Chrome's error "Cannot find
  context with specified id".

To keep runtime.ts small, the REPL bindings move to runtime/repl.ts and the
global lexical names to runtime/lexical-names.ts, both taking the realm
(window and its eval) instead of using the globals; their logic is
otherwise unchanged.
… style sheet

CSS.createStyleSheet ignored its frameId and always made one sheet in
the top document. Like Chrome, it now creates the inspector <style> in
the document of the frame asked for, each frame (document) reusing its
own one, and with `force` a new one every call; an unknown frame fails
with "Frame not found", Chrome's message. The registry keeps inspector
sheets as a set plus the reused one per document, instead of a single
inspectorSheet, and its id survives setStyleSheetText as before.

The editing commands work on iframe nodes and sheets: inline style edits
(setStyleTexts, setEffectivePropertyValueForNode) and setStyleSheetText
check the element / <style> with core/realm.ts instead of instanceof of
the top page, so an iframe's elements are no longer rejected.

Existing tests passed a made-up frameId to createStyleSheet; they now
pass the top frame's. New tests, against Chrome where both sides can act
on the page: per-frame reuse and `force`, headers of inspector sheets
in an iframe (frame, URL, isInline), the error for an unknown frame;
and on our side addRule, setStyleTexts, setRuleSelector and an inline
edit applying inside the iframe.

Not changed: as for the top frame, our inspector sheets are announced
without ownerNode and as mutable, where Chrome gives the <style> as
owner and isMutable false.
Input.dispatchMouseEvent (and touch emulation) hit tested the top
document only, so input over an iframe went to the <iframe> element with
top-page coordinates, and keys went to the top document's activeElement
(the iframe itself when something inside it had focus).

Mouse and touch input now follow core/geometry's hitPath: the element in
each document from the top one in, into same-origin iframes at the point
mapped through the iframe's content box. Events go to the innermost
element, with clientX/Y in its document's viewport (pageX/Y from that
window's scroll), view set to its window, and are built with that realm's
MouseEvent, PointerEvent, WheelEvent, Touch and TouchEvent, so the
iframe's own instanceof checks hold. Measured against Chrome:

- Hover is tracked per document. Entering an iframe, the top document
  gets the out/over pair for the <iframe> element, then the iframe's
  document the over for its element; leaving, the inner document gets
  its out first (with the point mapped into it, possibly negative), then
  the top document's pair. Within a document Chrome fires pointerout,
  pointerover, mouseout, mouseover in that order; the agent fired each
  pointer event with its mouse event, now it follows Chrome.
- A tap's compatibility mouseover moves the hover the same way, mouse
  events only.
- Focusing on press, blurring, the default editing actions and
  insertText use the target's own document; keys and text go to the
  focused element of the focused frame (the activeElement of the top
  document, then of the document of each focused iframe). Chrome's
  window blur/focus pair on moving focus into the iframe comes from
  focus() itself.
- The wheel scrolls the scrollers of the iframe's document, then its
  viewport, and passes on to the parent document once a frame cannot
  scroll further.

isEditable, the scroll checks and the focus handling no longer use
instanceof or the global document/window (core/realm.ts).

The input-iframe conformance test compares the events both documents
see (type, target, point, view and realm), the field value, focus and
scroll positions with Chrome's for moving in and out of an iframe and
clicking there, typing and insertText into its field, a tap with touch
emulation and the wheel over its scroller and document.
CSS.forcePseudoState copies the rules using a forced pseudo-class into a
constructed companion sheet adopted by each tree. For the tree of an
iframe that sheet has to come from the iframe's realm (a tree only
adopts sheets of its own realm), and its adoptedStyleSheets accessors
are those of the iframe's Document/ShadowRoot prototypes. Both now come
from the tree's window.

The media queries the companion depends on (whether a rule's @media
holds, and the queries watched for layer order changes) are evaluated
in the window of the tree being built and keyed by that window, so a
rule in an @media holding only in a narrow iframe is replayed there.

Like Chrome, which forgets a removed node's forced states, elements of
a document left without a window (its iframe removed, or navigated
away) are let go: removing the iframe does not disconnect them, so
being connected is not enough.

Tested against Chrome (each side forcing in turn): :hover and :focus on
an iframe element, with a :focus rule inside a narrow-only @media of the
iframe, matched rules and computed styles while forced and after.
…y work

core/geometry.ts and domains/input/input.ts no longer use instanceof, the global document/window or getComputedStyle (36 and 28 uses before); domains/overlay/overlay.ts is down from 16 to 8, the rest being the overlay layer, which belongs to the top document on purpose. Written by node scripts/check-realms.ts --update.
…al window

SheetRegistry typed its fetch as `typeof window.fetch`, two of the remaining uses of the top page's realm that scripts/check-realms.ts counts in the CSS domain. `typeof globalThis.fetch` is the same type. The counts are lowered accordingly; the one left in the CSS domain (style.ts parses declarations with a top-page <div>'s style, which does not depend on the realm) stays on purpose.
…dParams

Chrome replies -32602 to a uniqueContextId that names no context (read
off its raw reply); frameOf threw CdpError's default -32000. The
conformance tests compared messages only, so they also pin the codes
now, including callFunctionOn's target checks, which Chrome answers
with -32602 as well.
withCommandLineApi's release callback reads and deletes the utilities
on the frame's window before resolving `released`. A frame that
committed a cross-origin navigation in between throws SecurityError
there, so `released` never resolved and Runtime.evaluate or
callFunctionOn hung. The cleanup now runs in try/finally: the
utilities map is cleared and `released` resolves either way.
highlightMerged awaited the merged frame's box model and description, then
drew unconditionally, so an Overlay.hideHighlight or a new highlight that
arrived meanwhile was overdrawn by the stale one, which then stayed on the
page. The highlighter now counts every highlight and hide; the merged
highlight draws only if the count is unchanged after the await, so commands
take effect in order as in Chrome. Adds a test sending highlightNode and
hideHighlight together.
The set-up of both sides was made by the first test only, so a later test
run alone, or after the first failed midway, failed on the missing set-up
instead of its own behavior. The set-up is now made lazily, once per side,
by whichever test asks first.
… search

isFrameDocument repeated opaqueFrameOf's checks and called it, and emit then
called opaqueFrameOf again, searching the page's frames twice for a
sandboxed frame's document. childDocumentOf now returns both answers from a
single search, and emit uses it once per record.
…drop mergedDocument's pierce

The id-base helpers were inserted between NodeRegistry's JSDoc and the class,
leaving the comment attached to idBase; they now come before it. The pierce
argument of nodes.mergedDocument was never read (pierce is already folded
into the depth passed), so it is removed from the type and the call.
…endNodeId

DOM.pushNodesByBackendIdsToFrontend wrote each merged frame's answer back at
backendNodeIds.indexOf(id), so a repeated id only filled its first position
and the others stayed 0. The frame's ids are now grouped with their original
index and written back by it, as Chrome resolves each position on its own.
…nd destroy its contexts

A merged sandboxed iframe kept its namespace across documents (nsFor keyed
by element), so a reloaded document's agent numbered its node ids and
execution context ids from the same base again: old ids were routed to the
new document, and the new context was announced with the id of the old one,
which nobody had destroyed (the old agent unloads, and the page's Runtime
only follows FrameRegistry frames). Chrome destroys a navigated subframe's
context, then creates the next with a new id, and never reuses ids.

The handshake now takes a fresh namespace for each document (nextNs). The
router follows the contexts each frame's agent announces on a client's
session; when that session goes because the link or session closed, it
posts Runtime.executionContextDestroyed for them to clients with Runtime
enabled (not on the client's own reset).

The channel's message handler now parses each message once instead of up
to three times. Tests cover the above plus the fan-out timeout and late
answers, a page handler failing a fanned-out command, and the replay of
the last params of each kept command in first-sent order.
…n argument

The route table said an argument of the other side is unknown "as across processes in Chrome", the implementation that the sandboxed iframe "shares the page's process" in Chrome. The rule is V8's JavaScript world check, not about processes: both comments now say so.
… context

The same-origin iframe test only checked that the iframe script's executionContextId differed from the top one, so a stale or foreign id passed. It now finds the iframe's default context in Runtime.executionContextCreated by frameId and requires scriptParsed to name it, on both sides.

The sandboxed console test took the first consoleAPICalled after the evaluation, whatever logged it, and only compared the context flags between sides. It now matches the frame's own message and asserts it is in the frame's context. The exception is matched by context: ours reports it with a null exception, so its text cannot be matched.
…ble and navigate opaque frames by id

withMergedUrls chains each opaque frame change on a queue that may wait
up to twice URL_TIMEOUT for a merged frame's agent to tell its URL, then
called the listener unconditionally. Page.disable only unsubscribed from
new changes, so a navigation already queued was still reported on the
disabled session. withMergedUrls now returns a stop function alongside
the listener, and queued changes are dropped once Page is disabled or
the session resets, as Chrome reports no Page event after disable.

Page.navigate looked the frame up among same-origin frames only, so a
sandboxed iframe's opaque frame, listed in the frame tree under the
page's id as in Chrome, failed with 'No frame with given id found'.
Chrome navigates it. The opaque frame is now found in the tree and its
window's location set, which the parent may do cross-origin; the events
match Chrome's (one frameNavigated, no src change).

The sandboxed-frames conformance test gains both cases, and its weak
spots are tightened: the own-navigation case asserts frameStoppedLoading
was seen and frameNavigated came exactly once; the pushState case polls
for its event instead of a fixed 300 ms sleep; the non-merged case turns
auto-attach on for our side too and asserts no child target there.
…s in one history

Edits of a merged frame's nodes were routed to its agent and recorded in its
own history, while DOM.undo/redo/markUndoableState only touched the page's,
so Ctrl+Z after editing inside a sandboxed iframe did nothing yet succeeded.
Chrome keeps such a frame in the page's target with a single InspectorHistory
(checked: page and frame edits undo and redo in the order they were made).

merged-frames gains onRouted, called after a routed command succeeds. For DOM
and CSS edits (and DOM.moveTo/copyTo sent by dom.ts) the DOM domain marks a
step in the frame's agent and records in the page's history an action that
asks that agent for DOM.undo/DOM.redo; DOM.undo/redo await those replays.

Also: asks of merged.ts to frame agents now time out (FAN_OUT_TIMEOUT), so a
frame that stops answering no longer hangs getDocument, describeNode,
requestChildNodes or getNodeForLocation; a timed-out frame is treated as one
that answered an error. A frame's cached root is dropped when its link closes,
as namespaces are no longer reused. The quadsIn comment names its real fields.
…th Chrome

Covers setAttributeValue/removeAttribute/setOuterHTML and removeNode events
on the page's session and their node ids, pushNodesByBackendIdsToFrontend
with a repeated frame id, pushNodeByPathToFrontend into the frame, DOM.disable
then enable, and DOM.undo/redo across page and frame edits.
…ith Chrome

Chrome destroys a reloading sandboxed iframe's execution context before
creating the next document's, with an id not seen before. The new
conformance test reloads the merged frame on both sides and compares
that order and the new id; it fails on the code before the namespace fix.
…nks again

With a fresh namespace per handshake, a document that links again without
reloading (its parent pinged after linking to its own parent, or back from
the back/forward cache) got another namespace. Its node ids and contexts are
renumbered or reset with the session, but ids it already handed out and
keeps for the document's life (backend node ids in NodeRegistry, stylesheet
ids cached per sheet) stayed in the old namespace, which no link serves any
more: commands carrying them went to the page's own handlers as unknown.

A merged document now tells, in its next hellos, the namespace it was
given; the parent gives it again only if it is the one that element's
frame was given last (MergedFrames.nsFor(owner, previous)). A new
document's fresh JS tells none and still gets a new namespace.
… its namespace

A pinged sandboxed iframe links again with the same document and must keep
its namespace, while its next document takes another; the test fails
without the namespace told in the hello. The merge test no longer expects
namespace 1: the page's first ping may cross the iframe's first hello,
which then takes the next one.
Describe per-document namespaces and destroyed contexts, the shared
undo history, the 1 s fan-out timeout, and that a sandboxed iframe with
a src of another origin (same-site too) is refused. Add roadmap items
for same-site sandboxed iframes and Debugger breakpoints.
…ndboxed iframe

A sandboxed document reports the null origin, so the parent trusts it by the
iframe's src only. After the iframe navigates or is redirected to another
origin, a document there that speaks the handshake is merged as well. Document
this limitation instead of tightening the handshake, which could not stop
redirects and would break in-frame reloads.
…breakpoints row

Breakpoint panels are not bundled, so resolving breakpoint locations has no
user-visible effect; remove the roadmap row. Sandboxed iframes with a
cross-origin src stay refused: trusting them rests on an unverifiable src and
the cases are rare, so move them from the roadmap to the shelved list.
Measured: errors thrown by the sandboxed document's own scripts carry their
exception object; only code run from the Console, evaluated through the probe
script, is muted as cross-origin by the opaque document, leaving exception
null.
… loaderId

pushState in a merged sandboxed iframe already reports navigatedWithinDocument,
and the frame URL is refreshed before any getFrameTree answer, so drop that
limitation. The initial about:blank context and a new loaderId per navigation
have no small, reliable fix; move them from the roadmap to the shelved list.
On Linux CI the sandboxed iframe's body starts at y 71, not the 75 measured
on macOS, so the Chrome-side spot check keeps only the event, target and x;
ours is still compared with Chrome in full. The Watch prompt could have a
bounding box with no size and fail to be clicked, so wait until it has one.
The public API reports gained only the probe's data-frame-origins and
data-parent-origins attributes and the Vite plugin's frameOrigins and
parentOrigins options. Also note the CDP commands that now fail instead of
succeeding, and the new internal relay methods that tie the probe and the
remote server to the same version.
@jerrychan7
jerrychan7 merged commit 3656080 into main Nov 4, 2026
1 check passed
@jerrychan7
jerrychan7 deleted the release/0.3.0 branch November 4, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant