Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/studio/src/components/editor/AnimationCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AnimationCard } from "./AnimationCard";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { EASE_PRESETS } from "./easePresetLibrary";

const trackStudioSegmentEaseEdit = vi.hoisted(() => vi.fn());
vi.mock("../../telemetry/events", () => ({ trackStudioSegmentEaseEdit }));

(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

afterEach(() => {
document.body.innerHTML = "";
trackStudioSegmentEaseEdit.mockClear();
});

function baseAnimation(overrides: Partial<GsapAnimation> = {}): GsapAnimation {
Expand All @@ -26,6 +31,102 @@ function baseAnimation(overrides: Partial<GsapAnimation> = {}): GsapAnimation {

const noop = () => {};

function selectPreset(host: HTMLElement, presetId: string): string {
const presetConfig = EASE_PRESETS.find((candidate) => candidate.id === presetId);
if (!presetConfig) throw new Error(`Missing ease preset: ${presetId}`);

const dropdown = host.querySelector<HTMLButtonElement>("[data-ease-type-dropdown]");
expect(dropdown).not.toBeNull();
act(() => dropdown?.click());

const preset = host.querySelector<HTMLButtonElement>(`[data-ease-preset-id="${presetId}"]`);
expect(preset).not.toBeNull();
act(() => preset?.click());
return presetConfig.ease;
}

function renderExpandedCard({
animation,
flat,
onUpdateMeta = vi.fn(),
onUpdateKeyframeEase = vi.fn(),
}: {
animation: GsapAnimation;
flat?: boolean;
onUpdateMeta?: ReturnType<typeof vi.fn>;
onUpdateKeyframeEase?: ReturnType<typeof vi.fn>;
}) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={animation}
defaultExpanded
flat={flat}
onUpdateProperty={noop}
onUpdateMeta={onUpdateMeta}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
onUpdateKeyframeEase={onUpdateKeyframeEase}
/>,
);
});
return { host, root };
}

describe("AnimationCard ease editing", () => {
it("commits one preset change to the selected keyframe segment", () => {
const onUpdateKeyframeEase = vi.fn();
const animation = baseAnimation({
keyframes: {
format: "percentage",
keyframes: [
{ percentage: 0, properties: { opacity: 0 } },
{ percentage: 50, properties: { opacity: 0.5 } },
{ percentage: 100, properties: { opacity: 1 } },
],
},
});
const view = renderExpandedCard({ animation, onUpdateKeyframeEase });

const segment = Array.from(view.host.querySelectorAll("button")).find((button) =>
button.textContent?.includes("0% → 50%"),
);
expect(segment).toBeDefined();
act(() => segment?.click());
const ease = selectPreset(view.host, "quad-out");

expect(onUpdateKeyframeEase).toHaveBeenCalledExactlyOnceWith(animation.id, 50, ease);
expect(trackStudioSegmentEaseEdit).toHaveBeenCalledExactlyOnceWith({
action: "commit",
ease,
});
act(() => view.root.unmount());
});

it("commits one preset change through flat tween metadata", () => {
const onUpdateMeta = vi.fn();
const onUpdateKeyframeEase = vi.fn();
const animation = baseAnimation({ id: "flat-tween" });
const view = renderExpandedCard({
animation,
flat: true,
onUpdateMeta,
onUpdateKeyframeEase,
});

const ease = selectPreset(view.host, "quad-out");

expect(onUpdateMeta).toHaveBeenCalledExactlyOnceWith(animation.id, { ease });
expect(onUpdateKeyframeEase).not.toHaveBeenCalled();
expect(trackStudioSegmentEaseEdit).not.toHaveBeenCalled();
act(() => view.root.unmount());
});
});

describe("AnimationCard flat branch", () => {
it("renders a mint border-left and panel-token colors when flat", () => {
const host = document.createElement("div");
Expand Down
7 changes: 5 additions & 2 deletions packages/studio/src/components/editor/AnimationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { memo, useCallback, useMemo, useState } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
import { trackStudioSegmentEaseEdit } from "../../telemetry/events";
import { RESPONSIVE_GRID } from "./propertyPanelHelpers";
import { MetricField, SelectField } from "./propertyPanelPrimitives";
import { controlPointsForGsapEase } from "./studioMotion";
Expand Down Expand Up @@ -264,7 +265,10 @@ export const AnimationCard = memo(function AnimationCard({
globalEase={animation.keyframes.easeEach ?? animation.ease ?? "none"}
expandedPct={expandedKfPct}
onToggle={setExpandedKfPct}
onEaseCommit={(pct, ease) => onUpdateKeyframeEase(animation.id, pct, ease)}
onEaseCommit={(pct, ease) => {
onUpdateKeyframeEase(animation.id, pct, ease);
trackStudioSegmentEaseEdit({ action: "commit", ease });
}}
onApplyAll={
onSetAllKeyframeEases
? (ease) => onSetAllKeyframeEases(animation.id, ease)
Expand Down Expand Up @@ -292,7 +296,6 @@ export const AnimationCard = memo(function AnimationCard({
/>
<EaseCurveSection
ease={easeName}
duration={animation.duration}
onCustomEaseCommit={(customEase) => {
const easeKey = animation.keyframes ? "easeEach" : "ease";
onUpdateMeta(animation.id, { [easeKey]: customEase });
Expand Down
Loading
Loading