Skip to content

Commit ada7498

Browse files
authored
Merge pull request #50 from objectstack-ai/copilot/complete-roadmap-development-yet-again
2 parents b3bf228 + 8c18d85 commit ada7498

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+5217
-52
lines changed

ROADMAP.md

Lines changed: 61 additions & 52 deletions
Large diffs are not rendered by default.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Tests for useAnimationProtocol – validates animation and transition
3+
* registration, removal, retrieval, and reduced-motion awareness.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
import { useAnimationProtocol, AnimationConfig, TransitionConfig } from "~/hooks/useAnimationProtocol";
7+
8+
const fadeIn: AnimationConfig = {
9+
type: "fade",
10+
duration: 300,
11+
delay: 0,
12+
easing: "ease-in-out",
13+
iterations: 1,
14+
};
15+
16+
const slideUp: TransitionConfig = {
17+
property: "translateY",
18+
from: 100,
19+
to: 0,
20+
duration: 250,
21+
easing: "ease-out",
22+
};
23+
24+
describe("useAnimationProtocol", () => {
25+
it("returns default initial state", () => {
26+
const { result } = renderHook(() => useAnimationProtocol());
27+
28+
expect(result.current.animations.size).toBe(0);
29+
expect(result.current.transitions.size).toBe(0);
30+
expect(result.current.motionConfig).toEqual({
31+
reducedMotion: false,
32+
prefersContrast: false,
33+
animationScale: 1,
34+
});
35+
expect(result.current.isReduced).toBe(false);
36+
});
37+
38+
it("registers and retrieves an animation", () => {
39+
const { result } = renderHook(() => useAnimationProtocol());
40+
41+
act(() => {
42+
result.current.registerAnimation("fadeIn", fadeIn);
43+
});
44+
45+
expect(result.current.animations.size).toBe(1);
46+
expect(result.current.getAnimation("fadeIn")).toEqual(fadeIn);
47+
});
48+
49+
it("removes an animation", () => {
50+
const { result } = renderHook(() => useAnimationProtocol());
51+
52+
act(() => {
53+
result.current.registerAnimation("fadeIn", fadeIn);
54+
});
55+
56+
act(() => {
57+
result.current.removeAnimation("fadeIn");
58+
});
59+
60+
expect(result.current.animations.size).toBe(0);
61+
expect(result.current.getAnimation("fadeIn")).toBeUndefined();
62+
});
63+
64+
it("registers and retrieves a transition", () => {
65+
const { result } = renderHook(() => useAnimationProtocol());
66+
67+
act(() => {
68+
result.current.registerTransition("slideUp", slideUp);
69+
});
70+
71+
expect(result.current.transitions.size).toBe(1);
72+
expect(result.current.getTransition("slideUp")).toEqual(slideUp);
73+
});
74+
75+
it("removes a transition", () => {
76+
const { result } = renderHook(() => useAnimationProtocol());
77+
78+
act(() => {
79+
result.current.registerTransition("slideUp", slideUp);
80+
});
81+
82+
act(() => {
83+
result.current.removeTransition("slideUp");
84+
});
85+
86+
expect(result.current.transitions.size).toBe(0);
87+
expect(result.current.getTransition("slideUp")).toBeUndefined();
88+
});
89+
90+
it("sets motion config and computes isReduced", () => {
91+
const { result } = renderHook(() => useAnimationProtocol());
92+
93+
act(() => {
94+
result.current.setMotionConfig({
95+
reducedMotion: true,
96+
prefersContrast: true,
97+
animationScale: 0.5,
98+
});
99+
});
100+
101+
expect(result.current.isReduced).toBe(true);
102+
expect(result.current.motionConfig.animationScale).toBe(0.5);
103+
});
104+
105+
it("returns undefined for unregistered ids", () => {
106+
const { result } = renderHook(() => useAnimationProtocol());
107+
108+
expect(result.current.getAnimation("nonexistent")).toBeUndefined();
109+
expect(result.current.getTransition("nonexistent")).toBeUndefined();
110+
});
111+
112+
it("replaces an existing animation config", () => {
113+
const { result } = renderHook(() => useAnimationProtocol());
114+
115+
act(() => {
116+
result.current.registerAnimation("fadeIn", fadeIn);
117+
});
118+
119+
const updated: AnimationConfig = { ...fadeIn, duration: 500 };
120+
121+
act(() => {
122+
result.current.registerAnimation("fadeIn", updated);
123+
});
124+
125+
expect(result.current.animations.size).toBe(1);
126+
expect(result.current.getAnimation("fadeIn")!.duration).toBe(500);
127+
});
128+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Tests for useBlankPageLayout – validates blank page layout management,
3+
* item addition, removal, reordering, updates, and template selection.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
import { useBlankPageLayout, LayoutItem } from "~/hooks/useBlankPageLayout";
7+
8+
describe("useBlankPageLayout", () => {
9+
it("returns default state initially", () => {
10+
const { result } = renderHook(() => useBlankPageLayout());
11+
12+
expect(result.current.items).toEqual([]);
13+
expect(result.current.template).toBe("blank");
14+
});
15+
16+
it("sets layout items", () => {
17+
const { result } = renderHook(() => useBlankPageLayout());
18+
19+
const items: LayoutItem[] = [
20+
{ id: "i1", type: "text", config: { content: "Hello" }, order: 0 },
21+
{ id: "i2", type: "image", config: { src: "img.png" }, order: 1 },
22+
];
23+
24+
act(() => {
25+
result.current.setItems(items);
26+
});
27+
28+
expect(result.current.items).toHaveLength(2);
29+
});
30+
31+
it("adds an item sorted by order", () => {
32+
const { result } = renderHook(() => useBlankPageLayout());
33+
34+
act(() => {
35+
result.current.addItem({ id: "i2", type: "image", config: {}, order: 2 });
36+
});
37+
38+
act(() => {
39+
result.current.addItem({ id: "i1", type: "text", config: {}, order: 1 });
40+
});
41+
42+
expect(result.current.items).toHaveLength(2);
43+
expect(result.current.items[0].id).toBe("i1");
44+
expect(result.current.items[1].id).toBe("i2");
45+
});
46+
47+
it("removes an item by id", () => {
48+
const { result } = renderHook(() => useBlankPageLayout());
49+
50+
act(() => {
51+
result.current.setItems([
52+
{ id: "i1", type: "text", config: {}, order: 0 },
53+
{ id: "i2", type: "image", config: {}, order: 1 },
54+
]);
55+
});
56+
57+
act(() => {
58+
result.current.removeItem("i1");
59+
});
60+
61+
expect(result.current.items).toHaveLength(1);
62+
expect(result.current.items[0].id).toBe("i2");
63+
});
64+
65+
it("moves an item to a new order", () => {
66+
const { result } = renderHook(() => useBlankPageLayout());
67+
68+
act(() => {
69+
result.current.setItems([
70+
{ id: "i1", type: "text", config: {}, order: 0 },
71+
{ id: "i2", type: "image", config: {}, order: 1 },
72+
{ id: "i3", type: "video", config: {}, order: 2 },
73+
]);
74+
});
75+
76+
act(() => {
77+
result.current.moveItem("i1", 5);
78+
});
79+
80+
expect(result.current.items[0].id).toBe("i2");
81+
expect(result.current.items[2].id).toBe("i1");
82+
expect(result.current.items[2].order).toBe(5);
83+
});
84+
85+
it("updates an item's configuration", () => {
86+
const { result } = renderHook(() => useBlankPageLayout());
87+
88+
act(() => {
89+
result.current.setItems([
90+
{ id: "i1", type: "text", config: { content: "Hello" }, order: 0 },
91+
]);
92+
});
93+
94+
act(() => {
95+
result.current.updateItem("i1", { config: { content: "Updated" } });
96+
});
97+
98+
expect(result.current.items[0].config).toEqual({ content: "Updated" });
99+
});
100+
101+
it("sets the template type", () => {
102+
const { result } = renderHook(() => useBlankPageLayout());
103+
104+
act(() => {
105+
result.current.setTemplate("dashboard");
106+
});
107+
108+
expect(result.current.template).toBe("dashboard");
109+
});
110+
111+
it("changes template without affecting items", () => {
112+
const { result } = renderHook(() => useBlankPageLayout());
113+
114+
act(() => {
115+
result.current.addItem({ id: "i1", type: "text", config: {}, order: 0 });
116+
});
117+
118+
act(() => {
119+
result.current.setTemplate("form");
120+
});
121+
122+
expect(result.current.template).toBe("form");
123+
expect(result.current.items).toHaveLength(1);
124+
});
125+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Tests for useComponentAnimation – validates per-component animation
3+
* registration, removal, partial updates, and computed component list.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
import { useComponentAnimation, ComponentAnimationConfig } from "~/hooks/useComponentAnimation";
7+
8+
const cardConfig: ComponentAnimationConfig = {
9+
componentId: "card-1",
10+
enter: { duration: 200, easing: { type: "ease_in" } },
11+
exit: { duration: 150, easing: { type: "ease_out" } },
12+
};
13+
14+
const buttonConfig: ComponentAnimationConfig = {
15+
componentId: "btn-1",
16+
hover: { duration: 100, easing: { type: "linear" } },
17+
disabled: false,
18+
};
19+
20+
describe("useComponentAnimation", () => {
21+
it("returns empty initial state", () => {
22+
const { result } = renderHook(() => useComponentAnimation());
23+
24+
expect(result.current.componentAnimations.size).toBe(0);
25+
expect(result.current.registeredComponents).toEqual([]);
26+
});
27+
28+
it("sets and retrieves a component animation", () => {
29+
const { result } = renderHook(() => useComponentAnimation());
30+
31+
act(() => {
32+
result.current.setComponentAnimation("card-1", cardConfig);
33+
});
34+
35+
expect(result.current.componentAnimations.size).toBe(1);
36+
expect(result.current.getComponentAnimation("card-1")).toEqual(cardConfig);
37+
expect(result.current.registeredComponents).toEqual(["card-1"]);
38+
});
39+
40+
it("removes a component animation", () => {
41+
const { result } = renderHook(() => useComponentAnimation());
42+
43+
act(() => {
44+
result.current.setComponentAnimation("card-1", cardConfig);
45+
result.current.setComponentAnimation("btn-1", buttonConfig);
46+
});
47+
48+
act(() => {
49+
result.current.removeComponentAnimation("card-1");
50+
});
51+
52+
expect(result.current.componentAnimations.size).toBe(1);
53+
expect(result.current.getComponentAnimation("card-1")).toBeUndefined();
54+
expect(result.current.registeredComponents).toEqual(["btn-1"]);
55+
});
56+
57+
it("updates only the enter animation", () => {
58+
const { result } = renderHook(() => useComponentAnimation());
59+
60+
act(() => {
61+
result.current.setComponentAnimation("card-1", cardConfig);
62+
});
63+
64+
const newEnter = { duration: 400, easing: { type: "spring" as const } };
65+
66+
act(() => {
67+
result.current.updateEnterAnimation("card-1", newEnter);
68+
});
69+
70+
const updated = result.current.getComponentAnimation("card-1")!;
71+
expect(updated.enter).toEqual(newEnter);
72+
expect(updated.exit).toEqual(cardConfig.exit);
73+
});
74+
75+
it("updates only the exit animation", () => {
76+
const { result } = renderHook(() => useComponentAnimation());
77+
78+
act(() => {
79+
result.current.setComponentAnimation("card-1", cardConfig);
80+
});
81+
82+
const newExit = { duration: 500, easing: { type: "ease_in_out" as const } };
83+
84+
act(() => {
85+
result.current.updateExitAnimation("card-1", newExit);
86+
});
87+
88+
const updated = result.current.getComponentAnimation("card-1")!;
89+
expect(updated.exit).toEqual(newExit);
90+
expect(updated.enter).toEqual(cardConfig.enter);
91+
});
92+
93+
it("updateEnterAnimation is a no-op for unregistered component", () => {
94+
const { result } = renderHook(() => useComponentAnimation());
95+
96+
act(() => {
97+
result.current.updateEnterAnimation("unknown", { duration: 100, easing: { type: "linear" } });
98+
});
99+
100+
expect(result.current.componentAnimations.size).toBe(0);
101+
});
102+
103+
it("returns undefined for unregistered component id", () => {
104+
const { result } = renderHook(() => useComponentAnimation());
105+
106+
expect(result.current.getComponentAnimation("nonexistent")).toBeUndefined();
107+
});
108+
109+
it("replaces an existing component config", () => {
110+
const { result } = renderHook(() => useComponentAnimation());
111+
112+
act(() => {
113+
result.current.setComponentAnimation("card-1", cardConfig);
114+
});
115+
116+
const updated: ComponentAnimationConfig = { ...cardConfig, disabled: true };
117+
118+
act(() => {
119+
result.current.setComponentAnimation("card-1", updated);
120+
});
121+
122+
expect(result.current.componentAnimations.size).toBe(1);
123+
expect(result.current.getComponentAnimation("card-1")!.disabled).toBe(true);
124+
});
125+
});

0 commit comments

Comments
 (0)