Skip to content
Open
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
134 changes: 134 additions & 0 deletions packages/studio/src/components/editor/AnimationCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// @vitest-environment happy-dom

import React, { act } from "react";
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";

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

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

function baseAnimation(overrides: Partial<GsapAnimation> = {}): GsapAnimation {
return {
id: "anim-1",
method: "to",
position: 0.8,
duration: 1.2,
ease: "power2.out",
properties: { opacity: 1 },
...overrides,
} as GsapAnimation;
}

const noop = () => {};

describe("AnimationCard flat branch", () => {
it("renders a mint border-left and panel-token colors when flat", () => {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
flat
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
const card = host.querySelector('[data-flat-effect-card="true"]');
expect(card).not.toBeNull();
expect(card?.className).toContain("border-panel-accent");
act(() => root.unmount());
});

it("still renders the legacy (non-flat) appearance when flat is omitted", () => {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
expect(host.querySelector('[data-flat-effect-card="true"]')).toBeNull();
expect(host.textContent).toContain("power2.out");
act(() => root.unmount());
});

it("toggles expanded state when the collapsed header button is clicked, in both modes", () => {
for (const flat of [false, true]) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={false}
flat={flat || undefined}
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={noop}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
expect(host.textContent).not.toContain("Remove");
const button = host.querySelector("button");
expect(button).not.toBeNull();
act(() => {
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(host.textContent).toContain("Remove");
act(() => root.unmount());
}
});

it("invokes onDeleteAnimation with the animation id when Remove is clicked, in flat mode", () => {
const onDeleteAnimation = vi.fn();
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
act(() => {
root.render(
<AnimationCard
animation={baseAnimation()}
defaultExpanded={true}
flat
onUpdateProperty={noop}
onUpdateMeta={noop}
onDeleteAnimation={onDeleteAnimation}
onAddProperty={noop}
onRemoveProperty={noop}
/>,
);
});
const buttons = Array.from(host.querySelectorAll("button"));
const removeButton = buttons.find((b) => b.textContent === "Remove");
expect(removeButton).not.toBeUndefined();
act(() => {
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onDeleteAnimation).toHaveBeenCalledWith("anim-1");
act(() => root.unmount());
});
});
23 changes: 19 additions & 4 deletions packages/studio/src/components/editor/AnimationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import {
interface AnimationCardProps extends GsapAnimationEditCallbacks {
animation: GsapAnimation;
defaultExpanded: boolean;
flat?: boolean;
}

// fallow-ignore-next-line complexity
export const AnimationCard = memo(function AnimationCard({
animation,
defaultExpanded,
flat,
onUpdateProperty,
onUpdateMeta,
onDeleteAnimation,
Expand Down Expand Up @@ -150,7 +152,14 @@ export const AnimationCard = memo(function AnimationCard({
);

return (
<div className="border-b border-neutral-800 pb-3">
<div
data-flat-effect-card={flat ? "true" : undefined}
className={
flat
? "border-b border-l-2 border-panel-accent/40 border-b-panel-hairline pb-3 pl-2"
: "border-b border-neutral-800 pb-3"
}
>
<button
type="button"
onClick={() => setExpanded((v) => !v)}
Expand All @@ -162,21 +171,27 @@ export const AnimationCard = memo(function AnimationCard({
>
{methodLabel}
</span>
<span className="text-[11px] font-medium text-neutral-400" title="When this effect plays">
<span
className={`text-[11px] font-medium ${flat ? "text-panel-text-3" : "text-neutral-400"}`}
title="When this effect plays"
>
{typeof animation.position === "number"
? `${parseFloat(animation.position.toFixed(3))}s`
: animation.position}{" "}
– {typeof endTime === "number" ? `${parseFloat(endTime.toFixed(3))}s` : endTime}
</span>
<span className="ml-auto text-[10px] text-neutral-500" title={easeName}>
<span
className={`ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`}
title={easeName}
>
{easeLabel}
</span>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="currentColor"
className={`flex-shrink-0 text-neutral-500 transition-transform ${expanded ? "" : "-rotate-90"}`}
className={`flex-shrink-0 transition-transform ${flat ? "text-panel-text-5" : "text-neutral-500"} ${expanded ? "" : "-rotate-90"}`}
>
<path d="M2 3l3 4 3-4z" />
</svg>
Expand Down
Loading
Loading