fix(player): defer initial iframe navigation

This commit is contained in:
Vance Ingalls
2026-08-30 13:00:18 -07:00
committed by GitHub
parent 288df54e2e
commit 22b8100613
2 changed files with 38 additions and 0 deletions
@@ -525,6 +525,7 @@ describe("HyperframesPlayer shader transition options", () => {
const player = document.createElement("hyperframes-player") as PlayerWithIframe;
player.setAttribute("shader-capture-scale", "0.5");
player.setAttribute("shader-loading", "player");
document.body.appendChild(player);
player.setAttribute("src", "/api/projects/demo/preview?x=1#stage");
const url = new URL(player.iframeElement.src);
@@ -539,6 +540,7 @@ describe("HyperframesPlayer shader transition options", () => {
const player = document.createElement("hyperframes-player") as PlayerWithIframe;
player.setAttribute("shader-capture-scale", "0.5");
player.setAttribute("shader-loading", "player");
document.body.appendChild(player);
player.setAttribute(
"srcdoc",
'<!doctype html><html><head><script src="composition.js"></script></head><body></body></html>',
@@ -1512,6 +1514,36 @@ describe("HyperframesPlayer srcdoc attribute", () => {
player.remove();
});
it("does not navigate initial srcdoc before the runtime listener is connected", () => {
// React assigns custom-element attributes before inserting the element. If the observed
// attribute callback navigates the child iframe immediately, a fast srcdoc runtime can post
// its one-shot `ready` message before connectedCallback subscribes to `window.message`.
// Retained runtime data then waits forever and a caption style appears stuck on its bootstrap
// frame. The connect path owns the first navigation; attributeChangedCallback owns only
// subsequent swaps.
const player = document.createElement("hyperframes-player") as PlayerInternal;
player.setAttribute("srcdoc", "<!doctype html><html><body>deferred</body></html>");
expect(player.iframe.hasAttribute("srcdoc")).toBe(false);
document.body.appendChild(player);
expect(player.iframe.getAttribute("srcdoc")).toContain("<body>deferred</body>");
player.remove();
});
it("does not navigate initial src before the runtime listener is connected", () => {
const player = document.createElement("hyperframes-player") as PlayerInternal;
player.setAttribute("src", "/api/projects/deferred/preview");
expect(player.iframe.hasAttribute("src")).toBe(false);
document.body.appendChild(player);
expect(player.iframe.getAttribute("src")).toBe("/api/projects/deferred/preview");
player.remove();
});
it("forwards a srcdoc attribute set after connect to the iframe", () => {
// The composition-switching flow: same player element, new HTML.
// Without `attributeChangedCallback` wiring this would no-op.
@@ -218,6 +218,11 @@ class HyperframesPlayer extends HTMLElement {
attributeChangedCallback(name: string, oldVal: string | null, val: string | null) {
switch (name) {
case "src":
// Custom-element attributes are normally assigned before insertion (React does this for
// every render). Navigating the inner iframe here would let its one-shot runtime `ready`
// message fire before connectedCallback installs the parent message listener. Initial
// attributes are applied below by connectedCallback; only live changes navigate here.
if (!this.isConnected) break;
if (val) {
this._ready = false;
this._runtimeBridgeReady = false;
@@ -228,6 +233,7 @@ class HyperframesPlayer extends HTMLElement {
}
break;
case "srcdoc":
if (!this.isConnected) break;
this._ready = false;
this._runtimeBridgeReady = false;
this._rejectAllRuntimeDataDeliveries(