mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 16:42:27 +00:00
fix(studio): extend resolver-shadow runtime-node filter to timing/GSAP/reorder chokepoints (#1839)
* fix(studio): source-filter recordResolverParity (async + sourceHfIdCount) * refactor(studio): extract cutover-eligibility checks to unblock filesize cap * fix(studio): pass on-disk source to recordResolverParity at timing/gsap/delete chokepoints * fix(studio): void the reorderElements recordResolverParity call (now async) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(studio): resolve max-review findings (telemetry race, await, reorder filter, count invariant) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e5ec4cf532
commit
c215a20c8e
@@ -0,0 +1,277 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { shouldUseSdkCutover } from "../utils/sdkCutover";
|
||||
import type { PatchOperation } from "../utils/sourcePatcher";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { UseDomEditSessionParams } from "./useDomEditSession";
|
||||
|
||||
const styleOp = (property: string, value: string): PatchOperation => ({
|
||||
type: "inline-style",
|
||||
property,
|
||||
value,
|
||||
});
|
||||
|
||||
const attrOp = (property: string, value: string): PatchOperation => ({
|
||||
type: "attribute",
|
||||
property,
|
||||
value,
|
||||
});
|
||||
|
||||
describe("shouldUseSdkCutover", () => {
|
||||
it("returns false when flag is disabled", () => {
|
||||
expect(shouldUseSdkCutover(false, true, "hf-abc", [styleOp("color", "red")])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when no SDK session", () => {
|
||||
expect(shouldUseSdkCutover(true, false, "hf-abc", [styleOp("color", "red")])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when selection has no hfId", () => {
|
||||
expect(shouldUseSdkCutover(true, true, null, [styleOp("color", "red")])).toBe(false);
|
||||
expect(shouldUseSdkCutover(true, true, undefined, [styleOp("color", "red")])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when ops array is empty", () => {
|
||||
expect(shouldUseSdkCutover(true, true, "hf-abc", [])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when all conditions met with supported op types", () => {
|
||||
expect(shouldUseSdkCutover(true, true, "hf-abc", [styleOp("color", "red")])).toBe(true);
|
||||
expect(
|
||||
shouldUseSdkCutover(true, true, "hf-abc", [styleOp("color", "red"), attrOp("data-x", "1")]),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── onReorderShadow source filter (Fix 3) ────────────────────────────────────
|
||||
//
|
||||
// useDomEditSession composes ~9 sub-hooks; the only one relevant to this fix is
|
||||
// useDomEditCommits, which receives the onReorderShadow callback built inline.
|
||||
// Every other sub-hook is stubbed to a minimal shape so the hook under test can
|
||||
// render without pulling in unrelated preview/GSAP/selection machinery.
|
||||
|
||||
const recordResolverParity = vi.fn<(...args: unknown[]) => Promise<void>>(async () => {});
|
||||
const capturedOnReorderShadow: { fn: ((targets: string[]) => void) | undefined } = {
|
||||
fn: undefined,
|
||||
};
|
||||
|
||||
vi.mock("../utils/sdkResolverShadow", () => ({
|
||||
runResolverShadow: vi.fn(),
|
||||
recordResolverParity: (...args: unknown[]) => recordResolverParity(...args),
|
||||
}));
|
||||
vi.mock("./useDomEditCommits", () => ({
|
||||
useDomEditCommits: (params: { onReorderShadow?: (targets: string[]) => void }) => {
|
||||
capturedOnReorderShadow.fn = params.onReorderShadow;
|
||||
return {
|
||||
resolveImportedFontAsset: vi.fn(),
|
||||
handleDomStyleCommit: vi.fn(),
|
||||
handleDomAttributeCommit: vi.fn(),
|
||||
handleDomAttributeLiveCommit: vi.fn(),
|
||||
handleDomHtmlAttributeCommit: vi.fn(),
|
||||
handleDomTextCommit: vi.fn(),
|
||||
handleDomTextFieldStyleCommit: vi.fn(),
|
||||
handleDomAddTextField: vi.fn(),
|
||||
handleDomRemoveTextField: vi.fn(),
|
||||
handleDomBoxSizeCommit: vi.fn(),
|
||||
handleDomManualEditsReset: vi.fn(),
|
||||
handleDomEditElementDelete: vi.fn(),
|
||||
handleDomZIndexReorderCommit: vi.fn(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
vi.mock("./useDomSelection", () => ({
|
||||
useDomSelection: () => ({
|
||||
domEditSelection: null,
|
||||
domEditGroupSelections: [],
|
||||
domEditHoverSelection: null,
|
||||
activeGroupElement: null,
|
||||
domEditSelectionRef: { current: null },
|
||||
domEditGroupSelectionsRef: { current: [] },
|
||||
setActiveGroupElement: vi.fn(),
|
||||
applyDomSelection: vi.fn(),
|
||||
clearDomSelection: vi.fn(),
|
||||
buildDomSelectionFromTarget: vi.fn(),
|
||||
resolveDomSelectionFromPreviewPoint: vi.fn(),
|
||||
resolveAllDomSelectionsFromPreviewPoint: vi.fn(),
|
||||
updateDomEditHoverSelection: vi.fn(),
|
||||
buildDomSelectionForTimelineElement: vi.fn(),
|
||||
handleTimelineElementSelect: vi.fn(),
|
||||
refreshDomEditSelectionFromPreview: vi.fn(),
|
||||
applyMarqueeSelection: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./useAskAgentModal", () => ({
|
||||
useAskAgentModal: () => ({
|
||||
agentModalOpen: false,
|
||||
agentModalAnchorPoint: null,
|
||||
copiedAgentPrompt: null,
|
||||
agentPromptSelectionContext: null,
|
||||
setAgentModalOpen: vi.fn(),
|
||||
setAgentPromptSelectionContext: vi.fn(),
|
||||
setAgentModalAnchorPoint: vi.fn(),
|
||||
handleAskAgent: vi.fn(),
|
||||
handleAgentModalSubmit: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./useStudioSelectionPublisher", () => ({
|
||||
useStudioSelectionPublisher: () => {},
|
||||
}));
|
||||
vi.mock("./useGsapTweenCache", () => ({
|
||||
useGsapCacheVersion: () => ({ version: 0, bump: vi.fn() }),
|
||||
}));
|
||||
vi.mock("./useGsapScriptCommits", () => ({
|
||||
useGsapScriptCommits: () => ({
|
||||
commitMutation: vi.fn(),
|
||||
updateGsapProperty: vi.fn(),
|
||||
updateGsapMeta: vi.fn(),
|
||||
deleteGsapAnimation: vi.fn(),
|
||||
deleteAllForSelector: vi.fn(),
|
||||
addGsapAnimation: vi.fn(),
|
||||
addGsapProperty: vi.fn(),
|
||||
removeGsapProperty: vi.fn(),
|
||||
updateGsapFromProperty: vi.fn(),
|
||||
addGsapFromProperty: vi.fn(),
|
||||
removeGsapFromProperty: vi.fn(),
|
||||
addKeyframe: vi.fn(),
|
||||
addKeyframeBatch: vi.fn(),
|
||||
removeKeyframe: vi.fn(),
|
||||
moveKeyframe: vi.fn(),
|
||||
resizeKeyframedTween: vi.fn(),
|
||||
convertToKeyframes: vi.fn(),
|
||||
removeAllKeyframes: vi.fn(),
|
||||
setArcPath: vi.fn(),
|
||||
updateArcSegment: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./useGroupCommits", () => ({
|
||||
useGroupCommits: () => ({
|
||||
groupSelection: vi.fn(),
|
||||
ungroupSelection: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./useDomEditWiring", () => ({
|
||||
useDomEditWiring: () => ({
|
||||
onClickToSource: vi.fn(),
|
||||
selectedGsapAnimations: [],
|
||||
gsapMultipleTimelines: false,
|
||||
gsapUnsupportedTimelinePattern: false,
|
||||
trackGsapInteractionFailure: vi.fn(),
|
||||
makeFetchFallback: vi.fn(),
|
||||
handleGsapUpdateProperty: vi.fn(),
|
||||
handleGsapUpdateMeta: vi.fn(),
|
||||
handleGsapDeleteAnimation: vi.fn(),
|
||||
handleGsapDeleteAllForElement: vi.fn(),
|
||||
handleGsapAddAnimation: vi.fn(),
|
||||
handleGsapAddProperty: vi.fn(),
|
||||
handleGsapRemoveProperty: vi.fn(),
|
||||
handleGsapUpdateFromProperty: vi.fn(),
|
||||
handleGsapAddFromProperty: vi.fn(),
|
||||
handleGsapRemoveFromProperty: vi.fn(),
|
||||
handleGsapAddKeyframe: vi.fn(),
|
||||
handleGsapAddKeyframeBatch: vi.fn(),
|
||||
handleGsapRemoveKeyframe: vi.fn(),
|
||||
handleGsapMoveKeyframeToPlayhead: vi.fn(),
|
||||
handleGsapMoveKeyframe: vi.fn(),
|
||||
handleGsapResizeKeyframedTween: vi.fn(),
|
||||
handleGsapConvertToKeyframes: vi.fn(),
|
||||
handleGsapRemoveAllKeyframes: vi.fn(),
|
||||
handleResetSelectedElementKeyframes: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./usePreviewInteraction", () => ({
|
||||
usePreviewInteraction: () => ({
|
||||
handlePreviewCanvasMouseDown: vi.fn(),
|
||||
handlePreviewCanvasPointerMove: vi.fn(),
|
||||
handlePreviewCanvasPointerLeave: vi.fn(),
|
||||
handleBlockedDomMove: vi.fn(),
|
||||
handleDomManualDragStart: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock("./useGsapAwareEditing", () => ({
|
||||
useGsapAwareEditing: () => ({
|
||||
handleGsapAwarePathOffsetCommit: vi.fn(),
|
||||
handleGsapAwareGroupPathOffsetCommit: vi.fn(),
|
||||
handleGsapAwareBoxSizeCommit: vi.fn(),
|
||||
handleGsapAwareRotationCommit: vi.fn(),
|
||||
commitAnimatedProperty: vi.fn(),
|
||||
commitAnimatedProperties: vi.fn(),
|
||||
handleSetArcPath: vi.fn(),
|
||||
handleUpdateArcSegment: vi.fn(),
|
||||
handleUnroll: vi.fn(),
|
||||
commitMutation: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Tell React this is an act-capable environment so act(...) flushes effects.
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
describe("onReorderShadow source filter", () => {
|
||||
it("passes a readSource function as the 4th recordResolverParity argument when sdkSession and activeCompPath are set", async () => {
|
||||
const { useDomEditSession } = await import("./useDomEditSession");
|
||||
const readProjectFile = vi.fn(async (path: string) => `content of ${path}`);
|
||||
// Minimal opaque test double: no sub-hook under test actually calls into the
|
||||
// session (useDomEditCommits and recordResolverParity are both mocked above),
|
||||
// so it only needs to flow through as a reference.
|
||||
const sdkSession = {} as unknown as Composition;
|
||||
|
||||
function Probe() {
|
||||
const params: UseDomEditSessionParams = {
|
||||
projectId: "proj-1",
|
||||
activeCompPath: "index.html",
|
||||
isMasterView: false,
|
||||
compIdToSrc: new Map(),
|
||||
captionEditMode: false,
|
||||
compositionLoading: false,
|
||||
previewIframeRef: { current: null },
|
||||
timelineElements: [],
|
||||
setSelectedTimelineElementId: vi.fn(),
|
||||
setRightCollapsed: vi.fn(),
|
||||
setRightPanelTab: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
refreshPreviewDocumentVersion: vi.fn(),
|
||||
queueDomEditSave: vi.fn(async (save: () => Promise<void>) => save()),
|
||||
readProjectFile,
|
||||
writeProjectFile: vi.fn(async () => {}),
|
||||
updateEditingFileContent: vi.fn(),
|
||||
domEditSaveTimestampRef: { current: 0 },
|
||||
editHistory: { recordEdit: vi.fn(async () => {}) },
|
||||
fileTree: [],
|
||||
importedFontAssetsRef: { current: [] },
|
||||
projectDir: null,
|
||||
projectIdRef: { current: "proj-1" },
|
||||
previewIframe: null,
|
||||
refreshKey: 0,
|
||||
previewDocumentVersion: 0,
|
||||
rightPanelTab: "design",
|
||||
applyStudioManualEditsToPreviewRef: { current: async () => {} },
|
||||
syncPreviewHistoryHotkey: vi.fn(),
|
||||
reloadPreview: vi.fn(),
|
||||
setRefreshKey: vi.fn(),
|
||||
sdkSession,
|
||||
forceReloadSdkSession: vi.fn(),
|
||||
};
|
||||
useDomEditSession(params);
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = document.createElement("div");
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(<Probe />);
|
||||
});
|
||||
try {
|
||||
expect(capturedOnReorderShadow.fn).toBeTypeOf("function");
|
||||
capturedOnReorderShadow.fn?.(["hf-target"]);
|
||||
expect(recordResolverParity).toHaveBeenCalledWith(
|
||||
sdkSession,
|
||||
"hf-target",
|
||||
"reorderElements",
|
||||
expect.any(Function),
|
||||
);
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user