Skip to content

Commit f7aba35

Browse files
authored
Merge pull request #29 from objectstack-ai/copilot/complete-development-testing
2 parents 84206f3 + 3825212 commit f7aba35

16 files changed

+1524
-22
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Tests for useAnalyticsTracking – validates page view and action
3+
* tracking hooks wrapping the analytics tracker.
4+
*/
5+
import { renderHook } from "@testing-library/react-native";
6+
import { createAnalyticsTracker } from "~/lib/analytics";
7+
import { usePageView, useTrackAction } from "~/hooks/useAnalyticsTracking";
8+
9+
describe("usePageView", () => {
10+
it("tracks a page view on mount", () => {
11+
const tracker = createAnalyticsTracker();
12+
const spy = jest.spyOn(tracker, "trackPageView");
13+
14+
renderHook(() => usePageView("HomeScreen", undefined, tracker));
15+
16+
expect(spy).toHaveBeenCalledWith("HomeScreen", undefined);
17+
});
18+
19+
it("tracks with additional properties", () => {
20+
const tracker = createAnalyticsTracker();
21+
const spy = jest.spyOn(tracker, "trackPageView");
22+
23+
renderHook(() =>
24+
usePageView("DetailScreen", { recordId: "123" }, tracker),
25+
);
26+
27+
expect(spy).toHaveBeenCalledWith("DetailScreen", { recordId: "123" });
28+
});
29+
30+
it("re-tracks when screen name changes", () => {
31+
const tracker = createAnalyticsTracker();
32+
const spy = jest.spyOn(tracker, "trackPageView");
33+
34+
const { rerender } = renderHook(
35+
({ screen }: { screen: string }) =>
36+
usePageView(screen, undefined, tracker),
37+
{ initialProps: { screen: "ScreenA" } },
38+
);
39+
40+
expect(spy).toHaveBeenCalledTimes(1);
41+
42+
rerender({ screen: "ScreenB" });
43+
44+
expect(spy).toHaveBeenCalledTimes(2);
45+
expect(spy).toHaveBeenLastCalledWith("ScreenB", undefined);
46+
});
47+
48+
it("does not re-track when other props change", () => {
49+
const tracker = createAnalyticsTracker();
50+
const spy = jest.spyOn(tracker, "trackPageView");
51+
52+
const { rerender } = renderHook(
53+
({ screen }: { screen: string }) =>
54+
usePageView(screen, undefined, tracker),
55+
{ initialProps: { screen: "ScreenA" } },
56+
);
57+
58+
rerender({ screen: "ScreenA" }); // Same screen name
59+
60+
expect(spy).toHaveBeenCalledTimes(1);
61+
});
62+
});
63+
64+
describe("useTrackAction", () => {
65+
it("returns trackAction and track functions", () => {
66+
const tracker = createAnalyticsTracker();
67+
const { result } = renderHook(() => useTrackAction(tracker));
68+
69+
expect(typeof result.current.trackAction).toBe("function");
70+
expect(typeof result.current.track).toBe("function");
71+
});
72+
73+
it("trackAction calls tracker.trackAction()", () => {
74+
const tracker = createAnalyticsTracker();
75+
const spy = jest.spyOn(tracker, "trackAction");
76+
const { result } = renderHook(() => useTrackAction(tracker));
77+
78+
result.current.trackAction("button_click", { buttonId: "save" });
79+
80+
expect(spy).toHaveBeenCalledWith("button_click", { buttonId: "save" });
81+
});
82+
83+
it("track calls tracker.track()", () => {
84+
const tracker = createAnalyticsTracker();
85+
const spy = jest.spyOn(tracker, "track");
86+
const { result } = renderHook(() => useTrackAction(tracker));
87+
88+
result.current.track("custom_event", { key: "value" });
89+
90+
expect(spy).toHaveBeenCalledWith("custom_event", { key: "value" });
91+
});
92+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Tests for useBatchMutation – validates the SDK-compatible alias
3+
* for useBatchOperations.
4+
*/
5+
import { useBatchMutation } from "~/hooks/useBatchMutation";
6+
import { useBatchOperations } from "~/hooks/useBatchOperations";
7+
8+
describe("useBatchMutation (alias)", () => {
9+
it("is re-exported as useBatchOperations", () => {
10+
expect(useBatchMutation).toBe(useBatchOperations);
11+
});
12+
});
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Tests for useFeatureFlag – validates reactive feature flag hooks
3+
* that subscribe to flag changes via FeatureFlagManager.
4+
*/
5+
import { renderHook, act } from "@testing-library/react-native";
6+
import { createFeatureFlagManager } from "~/lib/feature-flags";
7+
import {
8+
useFeatureFlag,
9+
useFeatureFlagPayload,
10+
useFeatureFlags,
11+
} from "~/hooks/useFeatureFlag";
12+
13+
describe("useFeatureFlag", () => {
14+
it("returns false for unknown flags", () => {
15+
const mgr = createFeatureFlagManager();
16+
const { result } = renderHook(() => useFeatureFlag("nonexistent", mgr));
17+
expect(result.current).toBe(false);
18+
});
19+
20+
it("returns true for enabled flags", () => {
21+
const mgr = createFeatureFlagManager({
22+
defaults: [{ key: "dark_mode", enabled: true }],
23+
});
24+
const { result } = renderHook(() => useFeatureFlag("dark_mode", mgr));
25+
expect(result.current).toBe(true);
26+
});
27+
28+
it("returns false for disabled flags", () => {
29+
const mgr = createFeatureFlagManager({
30+
defaults: [{ key: "beta", enabled: false }],
31+
});
32+
const { result } = renderHook(() => useFeatureFlag("beta", mgr));
33+
expect(result.current).toBe(false);
34+
});
35+
36+
it("re-renders when flags are overridden", () => {
37+
const mgr = createFeatureFlagManager({
38+
defaults: [{ key: "feature_x", enabled: false }],
39+
});
40+
const { result } = renderHook(() => useFeatureFlag("feature_x", mgr));
41+
expect(result.current).toBe(false);
42+
43+
act(() => {
44+
mgr.overrideFlags([{ key: "feature_x", enabled: true }]);
45+
});
46+
47+
expect(result.current).toBe(true);
48+
});
49+
50+
it("cleans up subscription on unmount", () => {
51+
const mgr = createFeatureFlagManager({
52+
defaults: [{ key: "test", enabled: true }],
53+
});
54+
const { unmount } = renderHook(() => useFeatureFlag("test", mgr));
55+
56+
// Should not throw after unmount
57+
unmount();
58+
act(() => {
59+
mgr.overrideFlags([{ key: "test", enabled: false }]);
60+
});
61+
});
62+
});
63+
64+
describe("useFeatureFlagPayload", () => {
65+
it("returns undefined for flags without payload", () => {
66+
const mgr = createFeatureFlagManager({
67+
defaults: [{ key: "basic", enabled: true }],
68+
});
69+
const { result } = renderHook(() => useFeatureFlagPayload("basic", mgr));
70+
expect(result.current).toBeUndefined();
71+
});
72+
73+
it("returns the payload for a flag", () => {
74+
const mgr = createFeatureFlagManager({
75+
defaults: [
76+
{
77+
key: "experiment",
78+
enabled: true,
79+
payload: { variant: "B", color: "blue" },
80+
},
81+
],
82+
});
83+
const { result } = renderHook(() =>
84+
useFeatureFlagPayload("experiment", mgr),
85+
);
86+
expect(result.current).toEqual({ variant: "B", color: "blue" });
87+
});
88+
89+
it("updates when payload changes", () => {
90+
const mgr = createFeatureFlagManager({
91+
defaults: [
92+
{ key: "exp", enabled: true, payload: { version: 1 } },
93+
],
94+
});
95+
const { result } = renderHook(() => useFeatureFlagPayload("exp", mgr));
96+
expect(result.current).toEqual({ version: 1 });
97+
98+
act(() => {
99+
mgr.overrideFlags([
100+
{ key: "exp", enabled: true, payload: { version: 2 } },
101+
]);
102+
});
103+
104+
expect(result.current).toEqual({ version: 2 });
105+
});
106+
});
107+
108+
describe("useFeatureFlags", () => {
109+
it("returns all flags", () => {
110+
const mgr = createFeatureFlagManager({
111+
defaults: [
112+
{ key: "a", enabled: true },
113+
{ key: "b", enabled: false },
114+
],
115+
});
116+
const { result } = renderHook(() => useFeatureFlags(mgr));
117+
118+
expect(result.current.flags).toEqual([
119+
{ key: "a", enabled: true },
120+
{ key: "b", enabled: false },
121+
]);
122+
expect(typeof result.current.refresh).toBe("function");
123+
});
124+
125+
it("updates when flags change", () => {
126+
const mgr = createFeatureFlagManager({
127+
defaults: [{ key: "x", enabled: false }],
128+
});
129+
const { result } = renderHook(() => useFeatureFlags(mgr));
130+
expect(result.current.flags).toHaveLength(1);
131+
132+
act(() => {
133+
mgr.overrideFlags([
134+
{ key: "x", enabled: true },
135+
{ key: "y", enabled: true },
136+
]);
137+
});
138+
139+
expect(result.current.flags).toHaveLength(2);
140+
expect(result.current.flags[0].enabled).toBe(true);
141+
});
142+
});

0 commit comments

Comments
 (0)