fix(player): authenticate slideshow navigation senders (#3820)

* fix(player): authenticate slideshow navigation senders

* test: isolate playground message boundary imports
This commit is contained in:
James Russo
2026-09-09 15:25:38 -04:00
committed by GitHub
parent c6010f2eb8
commit 8d80530ff4
3 changed files with 35 additions and 5 deletions
@@ -199,6 +199,30 @@ describe("<hyperframes-slideshow>", () => {
el.remove();
});
it("accepts embedding-parent and self navigation but rejects other senders", () => {
const onNext = vi.fn();
const el = makeEl({ onNext });
const embedding = document.createElement("iframe");
const foreign = document.createElement("iframe");
document.body.append(embedding, foreign);
vi.stubGlobal("parent", embedding.contentWindow);
try {
for (const source of [foreign.contentWindow, null]) {
window.dispatchEvent(new MessageEvent("message", { source, data: { type: "next" } }));
}
expect(onNext).not.toHaveBeenCalled();
for (const source of [window, embedding.contentWindow]) {
window.dispatchEvent(new MessageEvent("message", { source, data: { type: "next" } }));
}
expect(onNext).toHaveBeenCalledTimes(2);
} finally {
vi.unstubAllGlobals();
embedding.remove();
foreign.remove();
el.remove();
}
});
it("handles postMessage next", () => {
const el = document.createElement("hyperframes-slideshow") as any;
document.body.appendChild(el);
@@ -215,7 +239,7 @@ describe("<hyperframes-slideshow>", () => {
currentSlide: { hotspots: [] },
nextSlide: null,
});
window.dispatchEvent(new MessageEvent("message", { data: { type: "next" } }));
window.dispatchEvent(new MessageEvent("message", { source: window, data: { type: "next" } }));
expect(nextCalled).toBe(true);
el.remove();
});
@@ -1981,7 +2005,7 @@ describe("<hyperframes-slideshow> Fix 7 — audience mode ignores window postMes
currentSlide: { hotspots: [] },
nextSlide: null,
});
window.dispatchEvent(new MessageEvent("message", { data: { type: "next" } }));
window.dispatchEvent(new MessageEvent("message", { source: window, data: { type: "next" } }));
expect(nextCalled).toBe(false);
el.remove();
});
@@ -2003,7 +2027,7 @@ describe("<hyperframes-slideshow> Fix 7 — audience mode ignores window postMes
currentSlide: { hotspots: [] },
nextSlide: null,
});
window.dispatchEvent(new MessageEvent("message", { data: { type: "next" } }));
window.dispatchEvent(new MessageEvent("message", { source: window, data: { type: "next" } }));
expect(nextCalled).toBe(true);
el.remove();
});
@@ -2474,7 +2498,7 @@ describe("<hyperframes-slideshow> Fix 4 — back affordance (postMessage only; c
backCalled = true;
},
});
window.dispatchEvent(new MessageEvent("message", { data: { type: "back" } }));
window.dispatchEvent(new MessageEvent("message", { source: window, data: { type: "back" } }));
expect(backCalled).toBe(true);
el.remove();
});
@@ -2488,7 +2512,7 @@ describe("<hyperframes-slideshow> Fix 4 — back affordance (postMessage only; c
},
});
el.setAttribute("mode", "audience");
window.dispatchEvent(new MessageEvent("message", { data: { type: "back" } }));
window.dispatchEvent(new MessageEvent("message", { source: window, data: { type: "back" } }));
expect(backCalled).toBe(false);
el.remove();
});
@@ -914,6 +914,7 @@ export class HyperframesSlideshow extends HTMLElement {
// fallow-ignore-next-line complexity
private onMessage = (e: MessageEvent): void => {
if (e.source !== window.parent && e.source !== window) return;
// Audience mode is driven by BroadcastChannel; ignore embed postMessage nav.
if (this.resolveMode() === "audience") return;
const data = e.data as { type?: unknown; slideIndex?: unknown } | null;
+5
View File
@@ -4,6 +4,11 @@ import { expect, it, vi } from "vitest";
// Keep initialization at the asynchronous persistence boundary while exercising
// the real static UI and its registered message listener.
// The paused initializer never opens a composition. Keep unrelated SDK/parser
// and raw animation-library transforms out of this message-boundary witness.
vi.mock("@hyperframes/sdk", () => ({ openComposition: vi.fn() }));
vi.mock("@hyperframes/core/gsap-parser-acorn", () => ({ parseGsapScriptAcorn: vi.fn() }));
vi.mock("gsap/dist/gsap.min.js?raw", () => ({ default: "" }));
vi.mock("./fileAdapter.js", () => ({
createFileAdapter: () => new Promise(() => {}),
}));