mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
fix(studio): resolve recent timeline regressions
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import {
|
||||
Timeline,
|
||||
formatTimelineTickLabel,
|
||||
@@ -54,6 +54,56 @@ describe("Timeline provider boundary", () => {
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("opens the keyframe context menu without seeking to that keyframe", () => {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
Object.defineProperty(host, "clientWidth", {
|
||||
configurable: true,
|
||||
value: 720,
|
||||
});
|
||||
|
||||
usePlayerStore.setState({
|
||||
duration: 4,
|
||||
timelineReady: true,
|
||||
currentTime: 0.25,
|
||||
selectedElementId: "clip-1",
|
||||
elements: [{ id: "clip-1", tag: "div", start: 0, duration: 4, track: 0 }],
|
||||
keyframeCache: new Map([
|
||||
[
|
||||
"clip-1",
|
||||
{
|
||||
format: "percentage",
|
||||
keyframes: [{ percentage: 50, properties: { x: 100 }, tweenPercentage: 50 }],
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
|
||||
const onSeek = vi.fn();
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(React.createElement(Timeline, { onSeek }));
|
||||
});
|
||||
|
||||
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]');
|
||||
expect(diamond).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
diamond!.dispatchEvent(
|
||||
new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
button: 2,
|
||||
clientX: 120,
|
||||
clientY: 40,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(onSeek).not.toHaveBeenCalled();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateTicks", () => {
|
||||
|
||||
@@ -487,8 +487,6 @@ export const Timeline = memo(function Timeline({
|
||||
if (el) {
|
||||
setSelectedElementId(elId);
|
||||
onSelectElement?.(el);
|
||||
const absTime = el.start + (pct / 100) * el.duration;
|
||||
onSeek?.(absTime);
|
||||
}
|
||||
const kfData = keyframeCache.get(elId);
|
||||
const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function pointerEvent(type: string, init: PointerEventInit): Event {
|
||||
if (typeof PointerEvent === "function") return new PointerEvent(type, init);
|
||||
return new MouseEvent(type, init);
|
||||
}
|
||||
|
||||
function renderDiamonds(onClickKeyframe = vi.fn()) {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
act(() => {
|
||||
root.render(
|
||||
<TimelineClipDiamonds
|
||||
keyframesData={{
|
||||
format: "percentage",
|
||||
keyframes: [
|
||||
{ percentage: 0, properties: { x: 0 } },
|
||||
{ percentage: 50, properties: { x: 100 } },
|
||||
],
|
||||
}}
|
||||
clipWidthPx={200}
|
||||
clipHeightPx={48}
|
||||
accentColor="#4ba3d2"
|
||||
isSelected
|
||||
currentPercentage={0}
|
||||
elementId="clip-1"
|
||||
selectedKeyframes={new Set()}
|
||||
onClickKeyframe={onClickKeyframe}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
return { host, root, onClickKeyframe };
|
||||
}
|
||||
|
||||
describe("TimelineClipDiamonds", () => {
|
||||
it("treats primary pointerup without drag as a keyframe click", () => {
|
||||
const { host, root, onClickKeyframe } = renderDiamonds();
|
||||
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]');
|
||||
expect(diamond).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
diamond!.dispatchEvent(pointerEvent("pointerup", { bubbles: true, button: 0 }));
|
||||
});
|
||||
|
||||
expect(onClickKeyframe).toHaveBeenCalledWith(50);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("does not treat secondary pointerup as a keyframe click", () => {
|
||||
const { host, root, onClickKeyframe } = renderDiamonds();
|
||||
const diamond = host.querySelector<HTMLButtonElement>('button[title="50%"]');
|
||||
expect(diamond).not.toBeNull();
|
||||
|
||||
act(() => {
|
||||
diamond!.dispatchEvent(pointerEvent("pointerup", { bubbles: true, button: 2 }));
|
||||
});
|
||||
|
||||
expect(onClickKeyframe).not.toHaveBeenCalled();
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
@@ -178,6 +178,7 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds({
|
||||
const d = dragRef.current;
|
||||
// No drag armed (canDrag false / non-primary press) → treat as a click.
|
||||
if (!d || d.kfKey !== kfKey) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.shiftKey) onShiftClickKeyframe?.(elementId, kf.percentage);
|
||||
else onClickKeyframe?.(kf.percentage);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user