mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
fix(studio): rebind paused preview after live edits (#3596)
This commit is contained in:
@@ -316,10 +316,15 @@ describe("toggleTimelineElementHidden", () => {
|
||||
const iframe = document.createElement("iframe");
|
||||
document.body.append(iframe);
|
||||
const seek = vi.fn();
|
||||
const forceTimelineRebind = vi.fn();
|
||||
const win = iframe.contentWindow;
|
||||
if (!win) throw new Error("Expected iframe contentWindow");
|
||||
const playerWindow: Window & { __player?: { seek?: (time: number) => void } } = win;
|
||||
const playerWindow: Window & {
|
||||
__player?: { seek?: (time: number) => void };
|
||||
__hfForceTimelineRebind?: () => void;
|
||||
} = win;
|
||||
playerWindow.__player = { seek };
|
||||
playerWindow.__hfForceTimelineRebind = forceTimelineRebind;
|
||||
|
||||
const files = new Map([
|
||||
[
|
||||
@@ -369,6 +374,7 @@ describe("toggleTimelineElementHidden", () => {
|
||||
expect(recordEdit).toHaveBeenCalledTimes(1);
|
||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide element");
|
||||
expect(seek).toHaveBeenCalledWith(1.25);
|
||||
expect(forceTimelineRebind).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#hero")?.hidden,
|
||||
).toBe(true);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { usePlayerStore, type TimelineElement } from "../player";
|
||||
import { reseekPreviewAtTime } from "../player/hooks/timelineSyncHydration";
|
||||
import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
|
||||
import { applySoftReloadFinalization } from "../utils/gsapSoftReload";
|
||||
import {
|
||||
timelineTrackOrder,
|
||||
trackDisplayNumber,
|
||||
@@ -106,11 +108,9 @@ function patchLiveHiddenState(
|
||||
}
|
||||
|
||||
export function reseekPreviewRuntime(iframe: HTMLIFrameElement | null): void {
|
||||
try {
|
||||
const win: (Window & { __player?: { seek?: (time: number) => void } }) | null =
|
||||
iframe?.contentWindow ?? null;
|
||||
win?.__player?.seek?.(usePlayerStore.getState().currentTime);
|
||||
} catch {}
|
||||
const store = usePlayerStore.getState();
|
||||
if (applySoftReloadFinalization(iframe, store.currentTime)) return;
|
||||
reseekPreviewAtTime({ seek: store.requestSeek }, store.currentTime);
|
||||
}
|
||||
|
||||
export function groupElementsByTargetPath(
|
||||
|
||||
@@ -981,6 +981,42 @@ describe("useDomEditCommits style persist handling", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rebinds the paused preview after a saved optimistic style commit", async () => {
|
||||
stubPatchFetch({
|
||||
ok: true,
|
||||
changed: true,
|
||||
matched: true,
|
||||
path: "index.html",
|
||||
version: '"sha256:changed"',
|
||||
});
|
||||
const { iframe, element } = createPreviewElement();
|
||||
const seek = vi.fn();
|
||||
const forceTimelineRebind = vi.fn();
|
||||
Object.defineProperty(iframe.contentWindow, "__player", {
|
||||
configurable: true,
|
||||
value: { seek },
|
||||
});
|
||||
Object.defineProperty(iframe.contentWindow, "__hfForceTimelineRebind", {
|
||||
configurable: true,
|
||||
value: forceTimelineRebind,
|
||||
});
|
||||
usePlayerStore.setState({ currentTime: 2.4 });
|
||||
const rendered = renderDomEditCommits(createSelection(element), iframe);
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
await rendered.hook.handleDomStyleCommit("color", "blue");
|
||||
});
|
||||
|
||||
expect(seek).toHaveBeenCalledWith(2.4);
|
||||
expect(forceTimelineRebind).toHaveBeenCalledTimes(1);
|
||||
expect(rendered.reloadPreview).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
rendered.cleanup();
|
||||
usePlayerStore.getState().reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("toasts and reverts a style commit when the patch request rejects", async () => {
|
||||
const { element, rendered, cleanup } = await commitStyleAgainst(new Error("network down"));
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "./useDomEditCommitsHelpers";
|
||||
import type { CutoverResult } from "../utils/sdkCutover";
|
||||
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
||||
import { reseekPreviewRuntime } from "./timelineTrackVisibility";
|
||||
interface RecordEditInput {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
@@ -154,6 +155,10 @@ export function useDomEditCommits({
|
||||
if (options?.shouldSave && !options.shouldSave()) return;
|
||||
|
||||
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
||||
const completePersistence = <T>(result: T, changed: boolean): T => {
|
||||
if (options?.skipRefresh && changed) reseekPreviewRuntime(previewIframeRef.current);
|
||||
return result;
|
||||
};
|
||||
|
||||
const readResponse = await fetch(
|
||||
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
||||
@@ -201,7 +206,10 @@ export function useDomEditCommits({
|
||||
if (cutover.status === "committed") {
|
||||
// SDK handled it — its in-memory doc is already current, so do NOT
|
||||
// forceReload (that would echo-reload the session we just wrote).
|
||||
return { sourceFile: targetPath, version: cutover.version, changed: true };
|
||||
return completePersistence(
|
||||
{ sourceFile: targetPath, version: cutover.version, changed: true },
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +257,12 @@ export function useDomEditCommits({
|
||||
throw new DomEditPersistUnresolvableError(targetPath);
|
||||
}
|
||||
warnDomEditPersistNoOp(selection, operations);
|
||||
return typeof patchData.path === "string" && typeof patchData.version === "string"
|
||||
? { sourceFile: patchData.path, version: patchData.version, changed: false }
|
||||
: undefined;
|
||||
return completePersistence(
|
||||
typeof patchData.path === "string" && typeof patchData.version === "string"
|
||||
? { sourceFile: patchData.path, version: patchData.version, changed: false }
|
||||
: undefined,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
const patchedContent =
|
||||
@@ -289,11 +300,14 @@ export function useDomEditCommits({
|
||||
if (!options?.skipRefresh) {
|
||||
reloadPreview();
|
||||
}
|
||||
return finalContent === patchedContent &&
|
||||
typeof patchData.path === "string" &&
|
||||
typeof patchData.version === "string"
|
||||
? { sourceFile: patchData.path, version: patchData.version, changed: true }
|
||||
: undefined;
|
||||
return completePersistence(
|
||||
finalContent === patchedContent &&
|
||||
typeof patchData.path === "string" &&
|
||||
typeof patchData.version === "string"
|
||||
? { sourceFile: patchData.path, version: patchData.version, changed: true }
|
||||
: undefined,
|
||||
true,
|
||||
);
|
||||
},
|
||||
[
|
||||
activeCompPath,
|
||||
@@ -305,6 +319,7 @@ export function useDomEditCommits({
|
||||
showToast,
|
||||
forceReloadSdkSession,
|
||||
onTrySdkPersist,
|
||||
previewIframeRef,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -290,6 +290,15 @@ export function resolveReloadSeekTime(input: {
|
||||
return Math.min(target, input.duration);
|
||||
}
|
||||
|
||||
type SeekablePreview = Pick<PlaybackAdapter, "seek">;
|
||||
|
||||
/** Re-run the current frame even when the runtime already reports that time. */
|
||||
export function reseekPreviewAtTime(preview: SeekablePreview, time: number): void {
|
||||
const target = Number.isFinite(time) && time > 0 ? time : 0;
|
||||
preview.seek(target > 0.001 ? Math.max(0, target - 0.001) : 0.001);
|
||||
preview.seek(target);
|
||||
}
|
||||
|
||||
export function seekAdapterToRestorePoint(
|
||||
adapter: PlaybackAdapter,
|
||||
pendingSeekRef: { current: number | null },
|
||||
@@ -303,8 +312,7 @@ export function seekAdapterToRestorePoint(
|
||||
});
|
||||
pendingSeekRef.current = null;
|
||||
if (storeSeek != null) usePlayerStore.getState().clearSeekRequest();
|
||||
adapter.seek(startTime > 0.001 ? Math.max(0, startTime - 0.001) : 0.001);
|
||||
adapter.seek(startTime);
|
||||
reseekPreviewAtTime(adapter, startTime);
|
||||
return startTime;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user