fix(runtime): the editor keeps waking during a drag on a preview it did not build (#3850)

The manual-edit gesture watch tested mutation targets with `instanceof
Element`. The composition body is adopted into the preview frame, so its
nodes answer to another realm's Element and the check is false for every
one of them: the watch never sees a gesture, and the paused transport it
gates does not wake while the user drags.

Routes the check through the runtime's structural predicate, which is
what the preview-guard lint added alongside it now requires. Main is red
on that lint for this line, so this also unbreaks it.

Test adopts an element from a second realm and asserts the marker is
seen and cleared; it fails with the identity check restored.
This commit is contained in:
Miguel Ángel
2026-09-10 16:29:32 +00:00
committed by GitHub
parent d4fba55a4e
commit c752b89bc9
2 changed files with 28 additions and 1 deletions
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { JSDOM } from "jsdom";
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../editing/draftMarkers";
import { createManualEditGestureWatch } from "./manualEditGestureWatch";
@@ -34,6 +35,28 @@ describe("manual-edit gesture watch", () => {
}
});
it("sees a gesture on an element adopted from another realm", async () => {
// The composition body is built in the editor window and moved into the
// preview document, so its nodes carry another realm's prototypes and
// `target instanceof Element` answers false for every one of them. A
// gesture on such an element must still be seen, or a parked transport
// never wakes while the user drags.
const foreign = new JSDOM("<!doctype html><html><body></body></html>");
const el = foreign.window.document.createElement("div");
document.body.appendChild(document.adoptNode(el as unknown as Node));
const watch = createManualEditGestureWatch(document, () => {});
try {
expect(el instanceof Element).toBe(false);
(el as unknown as Element).setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, "token");
expect(watch.isActive()).toBe(true);
await flush();
(el as unknown as Element).removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
expect(watch.isActive()).toBe(false);
} finally {
watch.disconnect();
}
});
it("stops reporting a gesture whose element was torn out mid-drag", async () => {
document.body.innerHTML = `<div id="a"></div>`;
const watch = createManualEditGestureWatch(document, () => {});
@@ -1,4 +1,5 @@
import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../editing/draftMarkers";
import { isElementNode } from "./domRealm";
export interface ManualEditGestureWatch {
/** True while any element in the document carries the gesture marker. */
@@ -53,7 +54,10 @@ export function createManualEditGestureWatch(
const ingest = (records: MutationRecord[]): void => {
for (const record of records) {
const target = record.target;
if (!(target instanceof Element)) continue;
// Structural, not `instanceof`: the composition body is adopted into the
// preview frame, so its nodes answer to another realm's Element and an
// identity check silently skips every gesture. See domRealm.ts.
if (!isElementNode(target)) continue;
if (target.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) marked.add(target);
else marked.delete(target);
}