mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +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");
|
const iframe = document.createElement("iframe");
|
||||||
document.body.append(iframe);
|
document.body.append(iframe);
|
||||||
const seek = vi.fn();
|
const seek = vi.fn();
|
||||||
|
const forceTimelineRebind = vi.fn();
|
||||||
const win = iframe.contentWindow;
|
const win = iframe.contentWindow;
|
||||||
if (!win) throw new Error("Expected 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.__player = { seek };
|
||||||
|
playerWindow.__hfForceTimelineRebind = forceTimelineRebind;
|
||||||
|
|
||||||
const files = new Map([
|
const files = new Map([
|
||||||
[
|
[
|
||||||
@@ -369,6 +374,7 @@ describe("toggleTimelineElementHidden", () => {
|
|||||||
expect(recordEdit).toHaveBeenCalledTimes(1);
|
expect(recordEdit).toHaveBeenCalledTimes(1);
|
||||||
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide element");
|
expect(recordEdit.mock.calls[0]?.[0]?.label).toBe("Hide element");
|
||||||
expect(seek).toHaveBeenCalledWith(1.25);
|
expect(seek).toHaveBeenCalledWith(1.25);
|
||||||
|
expect(forceTimelineRebind).toHaveBeenCalledTimes(1);
|
||||||
expect(
|
expect(
|
||||||
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#hero")?.hidden,
|
usePlayerStore.getState().elements.find((el) => el.key === "index.html:#hero")?.hidden,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { usePlayerStore, type TimelineElement } from "../player";
|
import { usePlayerStore, type TimelineElement } from "../player";
|
||||||
|
import { reseekPreviewAtTime } from "../player/hooks/timelineSyncHydration";
|
||||||
import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
|
import { useExpandedTimelineElements } from "../player/hooks/useExpandedTimelineElements";
|
||||||
|
import { applySoftReloadFinalization } from "../utils/gsapSoftReload";
|
||||||
import {
|
import {
|
||||||
timelineTrackOrder,
|
timelineTrackOrder,
|
||||||
trackDisplayNumber,
|
trackDisplayNumber,
|
||||||
@@ -106,11 +108,9 @@ function patchLiveHiddenState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function reseekPreviewRuntime(iframe: HTMLIFrameElement | null): void {
|
export function reseekPreviewRuntime(iframe: HTMLIFrameElement | null): void {
|
||||||
try {
|
const store = usePlayerStore.getState();
|
||||||
const win: (Window & { __player?: { seek?: (time: number) => void } }) | null =
|
if (applySoftReloadFinalization(iframe, store.currentTime)) return;
|
||||||
iframe?.contentWindow ?? null;
|
reseekPreviewAtTime({ seek: store.requestSeek }, store.currentTime);
|
||||||
win?.__player?.seek?.(usePlayerStore.getState().currentTime);
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupElementsByTargetPath(
|
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 () => {
|
it("toasts and reverts a style commit when the patch request rejects", async () => {
|
||||||
const { element, rendered, cleanup } = await commitStyleAgainst(new Error("network down"));
|
const { element, rendered, cleanup } = await commitStyleAgainst(new Error("network down"));
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
} from "./useDomEditCommitsHelpers";
|
} from "./useDomEditCommitsHelpers";
|
||||||
import type { CutoverResult } from "../utils/sdkCutover";
|
import type { CutoverResult } from "../utils/sdkCutover";
|
||||||
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
||||||
|
import { reseekPreviewRuntime } from "./timelineTrackVisibility";
|
||||||
interface RecordEditInput {
|
interface RecordEditInput {
|
||||||
label: string;
|
label: string;
|
||||||
kind: EditHistoryKind;
|
kind: EditHistoryKind;
|
||||||
@@ -154,6 +155,10 @@ export function useDomEditCommits({
|
|||||||
if (options?.shouldSave && !options.shouldSave()) return;
|
if (options?.shouldSave && !options.shouldSave()) return;
|
||||||
|
|
||||||
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
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(
|
const readResponse = await fetch(
|
||||||
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
||||||
@@ -201,7 +206,10 @@ export function useDomEditCommits({
|
|||||||
if (cutover.status === "committed") {
|
if (cutover.status === "committed") {
|
||||||
// SDK handled it — its in-memory doc is already current, so do NOT
|
// SDK handled it — its in-memory doc is already current, so do NOT
|
||||||
// forceReload (that would echo-reload the session we just wrote).
|
// 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);
|
throw new DomEditPersistUnresolvableError(targetPath);
|
||||||
}
|
}
|
||||||
warnDomEditPersistNoOp(selection, operations);
|
warnDomEditPersistNoOp(selection, operations);
|
||||||
return typeof patchData.path === "string" && typeof patchData.version === "string"
|
return completePersistence(
|
||||||
? { sourceFile: patchData.path, version: patchData.version, changed: false }
|
typeof patchData.path === "string" && typeof patchData.version === "string"
|
||||||
: undefined;
|
? { sourceFile: patchData.path, version: patchData.version, changed: false }
|
||||||
|
: undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchedContent =
|
const patchedContent =
|
||||||
@@ -289,11 +300,14 @@ export function useDomEditCommits({
|
|||||||
if (!options?.skipRefresh) {
|
if (!options?.skipRefresh) {
|
||||||
reloadPreview();
|
reloadPreview();
|
||||||
}
|
}
|
||||||
return finalContent === patchedContent &&
|
return completePersistence(
|
||||||
typeof patchData.path === "string" &&
|
finalContent === patchedContent &&
|
||||||
typeof patchData.version === "string"
|
typeof patchData.path === "string" &&
|
||||||
? { sourceFile: patchData.path, version: patchData.version, changed: true }
|
typeof patchData.version === "string"
|
||||||
: undefined;
|
? { sourceFile: patchData.path, version: patchData.version, changed: true }
|
||||||
|
: undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeCompPath,
|
activeCompPath,
|
||||||
@@ -305,6 +319,7 @@ export function useDomEditCommits({
|
|||||||
showToast,
|
showToast,
|
||||||
forceReloadSdkSession,
|
forceReloadSdkSession,
|
||||||
onTrySdkPersist,
|
onTrySdkPersist,
|
||||||
|
previewIframeRef,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,15 @@ export function resolveReloadSeekTime(input: {
|
|||||||
return Math.min(target, input.duration);
|
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(
|
export function seekAdapterToRestorePoint(
|
||||||
adapter: PlaybackAdapter,
|
adapter: PlaybackAdapter,
|
||||||
pendingSeekRef: { current: number | null },
|
pendingSeekRef: { current: number | null },
|
||||||
@@ -303,8 +312,7 @@ export function seekAdapterToRestorePoint(
|
|||||||
});
|
});
|
||||||
pendingSeekRef.current = null;
|
pendingSeekRef.current = null;
|
||||||
if (storeSeek != null) usePlayerStore.getState().clearSeekRequest();
|
if (storeSeek != null) usePlayerStore.getState().clearSeekRequest();
|
||||||
adapter.seek(startTime > 0.001 ? Math.max(0, startTime - 0.001) : 0.001);
|
reseekPreviewAtTime(adapter, startTime);
|
||||||
adapter.seek(startTime);
|
|
||||||
return startTime;
|
return startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user