mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(slideshow): finish remaining split-PR review findings (#1594)
* fix(slideshow): address split-PR review findings on #1585 Genuinely-open findings from the #1580/#1590/#1591/#1592 reviews (the rest were already fixed on this branch: CSP handlers, manifest version, UUID ids, float keys, presenter 1s-timer): core (#1580): - isManifest rejects a non-object/array manifest (e.g. [42,null]) explicitly - resolveSlideshow flags duplicate slideSequence ids instead of silent overwrite player (#1590): - present() window.open uses noopener,noreferrer (audience syncs via channel) - BroadcastChannel name is per-deck (keyed on pathname) to avoid same-origin cross-talk between decks - add observedAttributes + attributeChangedCallback so runtime sound/mode toggles re-render studio (#1591/#1592): - persistSlideshowManifest no-op gate (skip write when HTML is unchanged) - surface persist failures (console.error) instead of silent .catch(()=>{}) - confirm before deleting a branch sequence (data-loss + dangling hotspots) + tests for the collision + non-object-manifest rejection. 20 core / 106 player / 53 studio pass; tsc/lint/fmt/fallow clean; deck still renders. * fix(slideshow): finish remaining split-PR review findings The larger items from the #1580/#1590/#1591/#1592 reviews (the rest landed in #1585): core (#1580): - dedup isSceneLikeCompositionId — shared slideshow/sceneId.ts, used by both the lint rule and the runtime scene-window computation (no more mirror-and-drift) player (#1590 / #1592): - onKey: when multiple decks share a page, drop the unfocused-convenience so a key drives only the focused deck - slow-iframe recovery: if the scene timeline posts after the wait times out (empty scenes), re-init once so sceneId slides resolve instead of being dropped studio (#1591): - persistSlideshowManifest validates the built island round-trips before writing - reorderBranchSlide helper + BranchTree up/down controls (parallel to main-line reorder), with a branch-position indicator + tests for reorderBranchSlide. core 228 / player 106 / studio (panel) 46 pass; tsc/lint/fmt/fallow clean. * fix(player,cli): use fileURLToPath for path resolution (Windows CI) new URL(...).pathname yields a leading-slash drive path ("/D:/...") on Windows, which broke: - packages/player/vitest.config.ts — the @hyperframes/core/slideshow alias resolved to a nonexistent path, failing the player slideshow tests on the Windows render-verification CI (passed on macOS/Linux where pathname is clean) - packages/cli/src/utils/compositionServer.ts helperDir — same bug in the play/present bundle-path resolution fileURLToPath converts file:// URLs to correct OS paths on all platforms. Player slideshow tests pass; present serves + resolves bundles. * fix(producer): fileURLToPath for the renders dir (Windows) DEFAULT_RENDERS_DIR used new URL(import.meta.url).pathname, which is "/D:/..." on Windows and resolves to a bogus path — affects the Windows render pipeline. Last of the .pathname -> fileURLToPath fixes (repo-wide src sweep now clean).
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { handleRuntimeMessage } from "../runtime-message-handler.js";
|
||||
import { dropInvalidSlides } from "./hyperframes-slideshow.js";
|
||||
import { slideshowChannelName } from "./slideshowPresenter.js";
|
||||
|
||||
// Dynamic import defers custom-element registration until happy-dom is active.
|
||||
// (Static top-level imports execute before the test environment is set up, which
|
||||
@@ -524,7 +525,7 @@ describe("<hyperframes-slideshow> presenter mode", () => {
|
||||
const tick = () => new Promise<void>((r) => setTimeout(r, 0));
|
||||
|
||||
it("audience mode: mirrors full position (sequence + slide + fragment) via syncTo", async () => {
|
||||
const presenterChannel = new BroadcastChannel("hf-slideshow");
|
||||
const presenterChannel = new BroadcastChannel(slideshowChannelName());
|
||||
const { el, getLastSync } = makeAudienceEl();
|
||||
|
||||
await tick();
|
||||
@@ -542,7 +543,7 @@ describe("<hyperframes-slideshow> presenter mode", () => {
|
||||
});
|
||||
|
||||
it("audience mode: mirrors a branch position too (full sequenceId forwarded to syncTo)", async () => {
|
||||
const presenterChannel = new BroadcastChannel("hf-slideshow");
|
||||
const presenterChannel = new BroadcastChannel(slideshowChannelName());
|
||||
const { el, getLastSync } = makeAudienceEl();
|
||||
|
||||
await tick();
|
||||
@@ -566,7 +567,7 @@ describe("<hyperframes-slideshow> presenter mode", () => {
|
||||
|
||||
it("presenter mode: posts position to channel on controller onChange", async () => {
|
||||
const received: unknown[] = [];
|
||||
const listenerChannel = new BroadcastChannel("hf-slideshow");
|
||||
const listenerChannel = new BroadcastChannel(slideshowChannelName());
|
||||
listenerChannel.onmessage = (e: MessageEvent) => received.push(e.data);
|
||||
|
||||
const { el, triggerChange } = makePresenterEl();
|
||||
@@ -608,7 +609,7 @@ describe("<hyperframes-slideshow> presenter mode", () => {
|
||||
await tick();
|
||||
|
||||
const received: unknown[] = [];
|
||||
const spy = new BroadcastChannel("hf-slideshow");
|
||||
const spy = new BroadcastChannel(slideshowChannelName());
|
||||
spy.onmessage = (e: MessageEvent) => received.push(e.data);
|
||||
|
||||
el.remove(); // triggers disconnectedCallback
|
||||
|
||||
@@ -114,6 +114,17 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Observe the attributes the component reads so runtime toggles take effect.
|
||||
static get observedAttributes(): string[] {
|
||||
return ["sound", "mode"];
|
||||
}
|
||||
|
||||
attributeChangedCallback(): void {
|
||||
// Re-render once bound so a flipped `sound`/`mode` is reflected (mute button,
|
||||
// audience-vs-presenter chrome). No-op before the controller binds.
|
||||
if (this.controller) this.render();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.disconnected = false;
|
||||
this.initInFlight = false;
|
||||
@@ -178,7 +189,9 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
*/
|
||||
present(): void {
|
||||
const sep = location.search ? "&" : "?";
|
||||
window.open(location.href + sep + "mode=audience", "_blank");
|
||||
// noopener,noreferrer: the audience window must not get a reference back to
|
||||
// this window (it syncs over BroadcastChannel, not window.opener).
|
||||
window.open(location.href + sep + "mode=audience", "_blank", "noopener,noreferrer");
|
||||
this.setAttribute("data-hf-presenting", "true");
|
||||
this.presenterStartMs = Date.now();
|
||||
if (this.presenterInterval === null) {
|
||||
@@ -278,6 +291,19 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
};
|
||||
|
||||
this.bindController(new SlideshowController(port, cleaned));
|
||||
|
||||
// Slow-iframe recovery: if the scene timeline hadn't posted yet (empty
|
||||
// scenes → sceneId-based slides were dropped), re-init once when it finally
|
||||
// arrives so those slides resolve instead of being permanently lost.
|
||||
if (scenes.length === 0 && manifest.slides.length > 0) {
|
||||
playerEl.addEventListener(
|
||||
"scenes",
|
||||
() => {
|
||||
if (gen === this.initGeneration) void this.init();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.initInFlight = false;
|
||||
}
|
||||
@@ -318,7 +344,10 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
// Arrows act even when nothing is focused (active === body/null) so a freshly
|
||||
// loaded deck responds without a click; Space/Backspace have strong page-level
|
||||
// defaults (scroll / history) so they only act when the deck actually has focus.
|
||||
const ambient = focused || active === document.body || active === null;
|
||||
// When several decks share a page, drop the unfocused-convenience so a key
|
||||
// doesn't drive every instance at once — only the focused deck responds.
|
||||
const multiInstance = document.querySelectorAll("hyperframes-slideshow").length > 1;
|
||||
const ambient = focused || (!multiInstance && (active === document.body || active === null));
|
||||
if (e.key === "ArrowRight") {
|
||||
if (!ambient) return;
|
||||
this.controller.next();
|
||||
|
||||
@@ -27,6 +27,16 @@ function isGotoMessage(data: unknown): data is GotoMessage {
|
||||
* Presenter (default) mode: posts position updates to the channel.
|
||||
* Audience mode: listens for goto messages and calls the provided handler.
|
||||
*/
|
||||
/**
|
||||
* Per-deck channel name. The presenter and its audience window load the same URL
|
||||
* (path), so keying on pathname keeps them paired while isolating other decks
|
||||
* presenting on the same origin (which would otherwise cross-talk on a fixed name).
|
||||
*/
|
||||
export function slideshowChannelName(): string {
|
||||
const path = typeof location !== "undefined" ? location.pathname : "";
|
||||
return `hf-slideshow:${path}`;
|
||||
}
|
||||
|
||||
export class SlideshowChannel {
|
||||
private channel: BroadcastChannel | null = null;
|
||||
|
||||
@@ -35,7 +45,7 @@ export class SlideshowChannel {
|
||||
private readonly onGoto: (msg: GotoMessage) => void,
|
||||
) {
|
||||
try {
|
||||
this.channel = new BroadcastChannel("hf-slideshow");
|
||||
this.channel = new BroadcastChannel(slideshowChannelName());
|
||||
} catch {
|
||||
// BroadcastChannel unavailable (e.g. unsupported env); degrade silently.
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { resolve } from "path";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const coreRoot = resolve(new URL("..", import.meta.url).pathname, "core/src");
|
||||
// fileURLToPath (not URL.pathname): on Windows .pathname yields "/D:/..." with a
|
||||
// leading slash, which breaks resolve() and the alias below.
|
||||
const coreRoot = resolve(fileURLToPath(new URL("../core/src", import.meta.url)));
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user