summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/animation/__tests__
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-06 01:23:47 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-06 01:23:47 +0200
commitfcc09e80e253831a163e2e4941542e0b34f80740 (patch)
tree4cc7aafd0756bb3fa23dfd41080237e462b5ecf2 /apps/web/src/lib/animation/__tests__
parentc56d6f8188cc1c0e72d75611269a8bda7b79e727 (diff)
parentdedb13546a16235e7d6649961d0c9b235f62aa34 (diff)
downloadopencut-fcc09e80e253831a163e2e4941542e0b34f80740.tar.gz
Merge branch 'temp'
Diffstat (limited to 'apps/web/src/lib/animation/__tests__')
-rw-r--r--apps/web/src/lib/animation/__tests__/binding-values.test.ts26
-rw-r--r--apps/web/src/lib/animation/__tests__/keyframe-query.test.ts139
2 files changed, 165 insertions, 0 deletions
diff --git a/apps/web/src/lib/animation/__tests__/binding-values.test.ts b/apps/web/src/lib/animation/__tests__/binding-values.test.ts
new file mode 100644
index 00000000..c5a354e4
--- /dev/null
+++ b/apps/web/src/lib/animation/__tests__/binding-values.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, test } from "bun:test";
+import {
+ composeAnimationValue,
+ createAnimationBinding,
+} from "@/lib/animation/binding-values";
+
+describe("binding values", () => {
+ test("formats composed animated colors as hex", () => {
+ const binding = createAnimationBinding({
+ path: "color",
+ kind: "color",
+ });
+
+ expect(
+ composeAnimationValue({
+ binding,
+ componentValues: {
+ r: 1,
+ g: 0,
+ b: 0,
+ a: 1,
+ },
+ }),
+ ).toBe("#ff0000");
+ });
+});
diff --git a/apps/web/src/lib/animation/__tests__/keyframe-query.test.ts b/apps/web/src/lib/animation/__tests__/keyframe-query.test.ts
new file mode 100644
index 00000000..319493c8
--- /dev/null
+++ b/apps/web/src/lib/animation/__tests__/keyframe-query.test.ts
@@ -0,0 +1,139 @@
+import { describe, expect, test } from "bun:test";
+import {
+ getElementKeyframes,
+ getKeyframeById,
+ getKeyframeAtTime,
+} from "@/lib/animation/keyframe-query";
+import type {
+ ElementAnimations,
+ ScalarAnimationKey,
+} from "@/lib/animation/types";
+
+function createScalarKey({
+ id,
+ time,
+ value,
+}: {
+ id: string;
+ time: number;
+ value: number;
+}): ScalarAnimationKey {
+ return {
+ id,
+ time,
+ value,
+ segmentToNext: "linear",
+ tangentMode: "flat",
+ };
+}
+
+function buildPositionAnimations({
+ xKeys,
+ yKeys,
+}: {
+ xKeys: ScalarAnimationKey[];
+ yKeys: ScalarAnimationKey[];
+}): ElementAnimations {
+ return {
+ bindings: {
+ "transform.position": {
+ path: "transform.position",
+ kind: "vector2",
+ components: [
+ { key: "x", channelId: "transform.position:x" },
+ { key: "y", channelId: "transform.position:y" },
+ ],
+ },
+ },
+ channels: {
+ "transform.position:x": {
+ kind: "scalar",
+ keys: xKeys,
+ },
+ "transform.position:y": {
+ kind: "scalar",
+ keys: yKeys,
+ },
+ },
+ };
+}
+
+describe("keyframe query", () => {
+ test("returns keyframes from any component channel", () => {
+ const animations = buildPositionAnimations({
+ xKeys: [createScalarKey({ id: "x-1", time: 1, value: 10 })],
+ yKeys: [createScalarKey({ id: "y-2", time: 2, value: 20 })],
+ });
+
+ expect(
+ getElementKeyframes({ animations }).map(({ id, time }) => ({
+ id,
+ time,
+ })),
+ ).toEqual([
+ { id: "x-1", time: 1 },
+ { id: "y-2", time: 2 },
+ ]);
+ });
+
+ test("finds a keyframe at time on a non-primary component", () => {
+ const animations = buildPositionAnimations({
+ xKeys: [createScalarKey({ id: "x-1", time: 1, value: 10 })],
+ yKeys: [createScalarKey({ id: "y-2", time: 2, value: 20 })],
+ });
+
+ expect(
+ getKeyframeAtTime({
+ animations,
+ propertyPath: "transform.position",
+ time: 2,
+ }),
+ ).toMatchObject({
+ id: "y-2",
+ time: 2,
+ });
+ });
+
+ test("finds a keyframe by id on a non-primary component", () => {
+ const animations = buildPositionAnimations({
+ xKeys: [createScalarKey({ id: "x-1", time: 1, value: 10 })],
+ yKeys: [createScalarKey({ id: "y-2", time: 2, value: 20 })],
+ });
+
+ expect(
+ getKeyframeById({
+ animations,
+ propertyPath: "transform.position",
+ keyframeId: "y-2",
+ }),
+ ).toMatchObject({
+ id: "y-2",
+ time: 2,
+ value: { x: 10, y: 20 },
+ });
+ });
+
+ test("prefers the primary component when multiple components share a time", () => {
+ const animations = buildPositionAnimations({
+ xKeys: [createScalarKey({ id: "x-1", time: 1, value: 10 })],
+ yKeys: [createScalarKey({ id: "y-1", time: 1, value: 20 })],
+ });
+
+ expect(
+ getElementKeyframes({ animations }).map(({ id, time }) => ({
+ id,
+ time,
+ })),
+ ).toEqual([{ id: "x-1", time: 1 }]);
+ expect(
+ getKeyframeAtTime({
+ animations,
+ propertyPath: "transform.position",
+ time: 1,
+ }),
+ ).toMatchObject({
+ id: "x-1",
+ time: 1,
+ });
+ });
+});