PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.2
Presto Player v4.3.2
4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / hooks / test / useEngagementChartData.spec.js
presto-player / src / admin / dashboard / hooks / test Last commit date
useCompleteOnboarding.spec.js 1 month ago useDateRangePicker.spec.js 2 months ago useEmail.spec.js 2 months ago useEngagementChartData.spec.js 2 months ago useLicenseSettings.spec.js 2 months ago useLink.spec.js 2 months ago useMediaDetail.spec.js 2 months ago useMediaLibrary.spec.js 2 months ago useMediaList.spec.js 2 months ago usePerformanceSettings.spec.js 2 months ago useRegisterActivePage.spec.js 2 months ago useSettingOption.spec.js 2 months ago useSimpleSettingsPage.spec.js 2 months ago useTopPerforming.spec.js 2 months ago useTopVideosPaginated.spec.js 2 months ago useUpgradeCTA.spec.js 2 months ago useUserDetail.spec.js 2 months ago
useEngagementChartData.spec.js
252 lines
1 import { renderHook, act } from "@testing-library/react-hooks";
2
3 // Stub the fetchers from ../utils so the hook's mount-time effect doesn't
4 // hit the network. Keep every other util (date helpers, formatters) real.
5 // Names must be `mock`-prefixed to satisfy jest.mock's out-of-scope rule.
6 const mockFetchViews = jest.fn();
7 const mockFetchWatchTime = jest.fn();
8
9 jest.mock("../../utils", () => {
10 const actual = jest.requireActual("../../utils");
11 return {
12 __esModule: true,
13 ...actual,
14 fetchViews: (...args) => mockFetchViews(...args),
15 fetchWatchTime: (...args) => mockFetchWatchTime(...args),
16 };
17 });
18
19 import useEngagementChartData from "../useEngagementChartData";
20
21 const okViews = (overrides = {}) => ({
22 total: 0,
23 chartData: [],
24 ...overrides,
25 });
26 const okWatch = (overrides = {}) => ({
27 total: 0,
28 chartData: [],
29 average: 0,
30 ...overrides,
31 });
32
33 beforeEach(() => {
34 mockFetchViews.mockReset();
35 mockFetchWatchTime.mockReset();
36 global.window.prestoPlayer = { isPremium: true };
37 });
38
39 afterEach(() => {
40 delete global.window.prestoPlayer;
41 });
42
43 describe("useEngagementChartData", () => {
44 describe("free tier (isPremium=false)", () => {
45 beforeEach(() => {
46 window.prestoPlayer.isPremium = false;
47 });
48
49 it("does not fire mount-time fetches and is not in a loading state", async () => {
50 const { result } = renderHook(() =>
51 useEngagementChartData({ showAllTimeSummary: false })
52 );
53 await act(async () => {});
54
55 expect(mockFetchViews).not.toHaveBeenCalled();
56 expect(mockFetchWatchTime).not.toHaveBeenCalled();
57 expect(result.current.isLoading).toBe(false);
58 expect(result.current.chartData).toEqual([]);
59 expect(result.current.total).toBe(0);
60 expect(result.current.error).toBeNull();
61 });
62 });
63
64 describe("premium tier", () => {
65 it("populates totals and clears loading after a successful mount fetch", async () => {
66 mockFetchViews.mockResolvedValue(okViews({ total: 42 }));
67 mockFetchWatchTime.mockResolvedValue(okWatch({ total: 360, average: 12 }));
68
69 const { result, waitForNextUpdate } = renderHook(() =>
70 useEngagementChartData({ showAllTimeSummary: false })
71 );
72
73 expect(result.current.isLoading).toBe(true);
74 await waitForNextUpdate();
75
76 expect(result.current.isLoading).toBe(false);
77 expect(result.current.error).toBeNull();
78 expect(result.current.total).toBe(42); // default tab is "views"
79 });
80
81 it("exposes the mount-fetch error only on the failing tab", async () => {
82 mockFetchViews.mockResolvedValue(okViews({ total: 10 }));
83 mockFetchWatchTime.mockRejectedValue(new Error("boom"));
84
85 const { result, waitForNextUpdate } = renderHook(() =>
86 useEngagementChartData({ showAllTimeSummary: false })
87 );
88 await waitForNextUpdate();
89
90 // Default tab (views) succeeded — no error shown so the user isn't
91 // warned about data they aren't looking at.
92 expect(result.current.activeTab).toBe("views");
93 expect(result.current.error).toBeNull();
94 expect(result.current.total).toBe(10);
95
96 // Switching to the failing tab surfaces an explicit watch-time error
97 // (rather than the previous generic "Some data could not be loaded").
98 act(() => {
99 result.current.setActiveTab("watch-time");
100 });
101 expect(result.current.error).toMatch(/watch[- ]?time/i);
102 expect(result.current.total).toBe(0);
103 });
104
105 it("exposes the mount-fetch error on the views tab when views rejects", async () => {
106 mockFetchViews.mockRejectedValue(new Error("nope"));
107 mockFetchWatchTime.mockResolvedValue(okWatch({ total: 99 }));
108
109 const { result, waitForNextUpdate } = renderHook(() =>
110 useEngagementChartData({ showAllTimeSummary: false })
111 );
112 await waitForNextUpdate();
113
114 // Default tab is "views" → reads the failed-half fallback (0) and
115 // surfaces the views-specific error.
116 expect(result.current.total).toBe(0);
117 expect(result.current.error).toMatch(/views/i);
118 });
119
120 it("fetches the all-time totals alongside the default-range chart data", async () => {
121 // Distinct totals per call so we can prove which fetch fed which
122 // field — `mockResolvedValueOnce` resolves in call order, so the
123 // first call (default-range) maps to the chart KPI and the second
124 // (all-time) maps to the summary card.
125 mockFetchViews
126 .mockResolvedValueOnce(okViews({ total: 100 }))
127 .mockResolvedValueOnce(okViews({ total: 9999 }));
128 mockFetchWatchTime
129 .mockResolvedValueOnce(okWatch({ total: 300 }))
130 .mockResolvedValueOnce(okWatch({ total: 8888 }));
131
132 const { result, waitForNextUpdate } = renderHook(() =>
133 useEngagementChartData({ showAllTimeSummary: true })
134 );
135 await waitForNextUpdate();
136
137 // The chart now defaults to the last-7-days window, so the
138 // "All Time …" summary cards need their own dedicated fetch in
139 // parallel — one call for the default range, one for all-time.
140 expect(mockFetchViews).toHaveBeenCalledTimes(2);
141 expect(mockFetchWatchTime).toHaveBeenCalledTimes(2);
142
143 // Chart KPI reflects the default-range fetch …
144 expect(result.current.total).toBe(100);
145 // … and the summary card reflects the dedicated all-time fetch,
146 // proving the two pipelines don't cross-wire.
147 expect(result.current.allTimeViews).toBe(9999);
148 expect(result.current.allTimeWatchTime).toBe(8888);
149
150 // Same wiring holds after switching tabs — the watch-time KPI
151 // should read from its own default-range total, not from all-time.
152 act(() => {
153 result.current.setActiveTab("watch-time");
154 });
155 expect(result.current.total).toBe(300);
156 });
157
158 it("keeps allTimeViews null when the all-time fetch fails, without surfacing a banner", async () => {
159 // The default-range fetch succeeds (chart renders normally) but
160 // the all-time fetch rejects. The Dashboard summary card should
161 // gracefully render nothing rather than crash on a null total,
162 // and the failure should be logged so it isn't entirely silent.
163 const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
164
165 mockFetchViews
166 .mockResolvedValueOnce(okViews({ total: 100 }))
167 .mockRejectedValueOnce(new Error("all-time views failed"));
168 mockFetchWatchTime
169 .mockResolvedValueOnce(okWatch({ total: 300 }))
170 .mockRejectedValueOnce(new Error("all-time watch-time failed"));
171
172 const { result, waitForNextUpdate } = renderHook(() =>
173 useEngagementChartData({ showAllTimeSummary: true })
174 );
175 await waitForNextUpdate();
176
177 expect(result.current.total).toBe(100);
178 expect(result.current.allTimeViews).toBeNull();
179 expect(result.current.allTimeWatchTime).toBeNull();
180 // Mount-level error banner is reserved for default-range failures,
181 // so the summary-card failure shouldn't poison the chart UX.
182 expect(result.current.error).toBeNull();
183 expect(warn).toHaveBeenCalled();
184
185 warn.mockRestore();
186 });
187
188 it("returns null all-time summary fields when showAllTimeSummary=false", async () => {
189 mockFetchViews.mockResolvedValue(okViews());
190 mockFetchWatchTime.mockResolvedValue(okWatch());
191
192 const { result, waitForNextUpdate } = renderHook(() =>
193 useEngagementChartData({ showAllTimeSummary: false })
194 );
195 await waitForNextUpdate();
196
197 expect(result.current.allTimeViews).toBeNull();
198 expect(result.current.allTimeWatchTime).toBeNull();
199 });
200 });
201
202 describe("public callbacks", () => {
203 beforeEach(async () => {
204 mockFetchViews.mockResolvedValue(okViews());
205 mockFetchWatchTime.mockResolvedValue(okWatch());
206 });
207
208 it("setSelectedDates(null) clears the selected range and any filtered result", async () => {
209 const { result, waitForNextUpdate } = renderHook(() =>
210 useEngagementChartData({ showAllTimeSummary: false })
211 );
212 await waitForNextUpdate();
213
214 const from = new Date(2026, 0, 1);
215 const to = new Date(2026, 0, 10);
216
217 await act(async () => {
218 result.current.setSelectedDates({ from, to });
219 });
220 // Wait for the date-filter fetch to settle so filteredResult is set.
221 await act(async () => {});
222
223 await act(async () => {
224 result.current.setSelectedDates(null);
225 });
226 expect(result.current.selectedDates).toEqual({ from: null, to: null });
227 // Cleared filteredResult means we fall back to the mount-data total.
228 expect(result.current.error).toBeNull();
229 });
230
231 it("setActiveTab switches between views and watch-time", async () => {
232 mockFetchViews.mockResolvedValueOnce(okViews({ total: 42 }));
233 mockFetchWatchTime.mockResolvedValueOnce(okWatch({ total: 360 }));
234
235 const { result, waitForNextUpdate } = renderHook(() =>
236 useEngagementChartData({ showAllTimeSummary: false })
237 );
238 await waitForNextUpdate();
239 expect(result.current.activeTab).toBe("views");
240 expect(result.current.total).toBe(42);
241
242 act(() => {
243 result.current.setActiveTab("watch-time");
244 });
245 expect(result.current.activeTab).toBe("watch-time");
246 expect(result.current.isWatchTime).toBe(true);
247 expect(result.current.total).toBe(360);
248 });
249
250 });
251 });
252