PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
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 / useUserDetail.spec.js
presto-player / src / admin / dashboard / hooks / test Last commit date
useCompleteOnboarding.spec.js 3 weeks ago useDateRangePicker.spec.js 1 month ago useEmail.spec.js 1 month ago useEngagementChartData.spec.js 1 month ago useLicenseSettings.spec.js 1 month ago useLink.spec.js 1 month ago useMediaDetail.spec.js 1 month ago useMediaLibrary.spec.js 1 month ago useMediaList.spec.js 1 month ago usePerformanceSettings.spec.js 1 month ago useRegisterActivePage.spec.js 1 month ago useSettingOption.spec.js 1 month ago useSimpleSettingsPage.spec.js 1 month ago useTopPerforming.spec.js 1 month ago useTopVideosPaginated.spec.js 1 month ago useUpgradeCTA.spec.js 1 month ago useUserDetail.spec.js 1 month ago
useUserDetail.spec.js
281 lines
1 import { renderHook, act } from "@testing-library/react-hooks";
2 import apiFetch from "@wordpress/api-fetch";
3 import useTopVideosPaginated from "../useTopVideosPaginated";
4 import useUserDetail from "../useUserDetail";
5
6 jest.mock("@wordpress/api-fetch");
7 jest.mock("../useTopVideosPaginated", () => ({
8 __esModule: true,
9 default: jest.fn(),
10 }));
11
12 const mediaStub = (overrides = {}) => ({
13 topMedia: [],
14 page: 1,
15 setPage: jest.fn(),
16 pagination: { totalItems: 0, totalPages: 0 },
17 perPage: 10,
18 isLoading: false,
19 error: null,
20 ...overrides,
21 });
22
23 // Drains pending React state updates so trailing setStats/setIsLoading
24 // don't fire after the test completes (which would trip @wordpress/jest-
25 // console's "console.error not expected" guard via React's act warning).
26 const settle = () => act(async () => {});
27
28 // Freeze "now" so the default-range math is deterministic — and in
29 // particular doesn't flake when the suite happens to straddle local
30 // midnight while the hook and the test each call `new Date()`.
31 beforeAll(() => {
32 jest.useFakeTimers().setSystemTime(new Date("2026-05-10T16:33:22"));
33 });
34
35 afterAll(() => {
36 jest.useRealTimers();
37 });
38
39 beforeEach(() => {
40 apiFetch.mockReset();
41 useTopVideosPaginated.mockReset();
42 useTopVideosPaginated.mockReturnValue(mediaStub());
43 });
44
45 describe("useUserDetail", () => {
46 it("does not fetch when userId is falsy", async () => {
47 const { result } = renderHook(() => useUserDetail(null));
48 await settle();
49 expect(apiFetch).not.toHaveBeenCalled();
50 expect(result.current.user).toBeNull();
51 });
52
53 it("populates user metadata from /wp/v2/users + parallel analytics", async () => {
54 apiFetch
55 .mockResolvedValueOnce({
56 id: 7,
57 name: "Ada Lovelace",
58 email: "ada@example.com",
59 avatar_urls: { 48: "small.png", 96: "big.png" },
60 })
61 .mockResolvedValueOnce({ view: "12" })
62 .mockResolvedValueOnce({ view: "30" })
63 .mockResolvedValueOnce({ view: "180" });
64
65 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(7));
66 await waitForNextUpdate();
67 await settle();
68
69 expect(result.current.user).toMatchObject({
70 id: 7,
71 name: "Ada Lovelace",
72 email: "ada@example.com",
73 avatarUrl: "big.png",
74 });
75 expect(result.current.user.profileUrl).toContain("user_id=7");
76
77 expect(result.current.stats).toEqual({
78 totalViews: 12,
79 avgWatchTime: "30s",
80 totalWatchTime: "3m",
81 });
82 });
83
84 it("falls back to a generated name when the user has none", async () => {
85 apiFetch
86 .mockResolvedValueOnce({ id: 99, name: "" })
87 .mockResolvedValueOnce({ view: "0" })
88 .mockResolvedValueOnce({ view: "0" })
89 .mockResolvedValueOnce({ view: "0" });
90
91 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(99));
92 await waitForNextUpdate();
93 await settle();
94
95 expect(result.current.user.name).toBe("User #99");
96 });
97
98 it("uses the smaller avatar size when 96 isn't available", async () => {
99 apiFetch
100 .mockResolvedValueOnce({
101 id: 1,
102 name: "x",
103 avatar_urls: { 48: "small.png" },
104 })
105 .mockResolvedValueOnce({ view: "0" })
106 .mockResolvedValueOnce({ view: "0" })
107 .mockResolvedValueOnce({ view: "0" });
108
109 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
110 await waitForNextUpdate();
111 await settle();
112 expect(result.current.user.avatarUrl).toBe("small.png");
113 });
114
115 it("forces avg/total watch time to '0s' when totalViews is 0", async () => {
116 apiFetch
117 .mockResolvedValueOnce({ id: 1, name: "x" })
118 .mockResolvedValueOnce({ view: "0" })
119 .mockResolvedValueOnce({ view: "100" })
120 .mockResolvedValueOnce({ view: "200" });
121
122 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
123 await waitForNextUpdate();
124 await settle();
125
126 expect(result.current.stats).toEqual({
127 totalViews: 0,
128 avgWatchTime: "0s",
129 totalWatchTime: "0s",
130 });
131 });
132
133 it("falls back to zero stats with a warn when analytics endpoints fail", async () => {
134 const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
135
136 apiFetch
137 .mockResolvedValueOnce({ id: 1, name: "x" })
138 .mockRejectedValueOnce(new Error("403"));
139
140 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
141 await waitForNextUpdate();
142 await settle();
143
144 expect(result.current.stats).toEqual({
145 totalViews: 0,
146 avgWatchTime: "0s",
147 totalWatchTime: "0s",
148 });
149 expect(warnSpy).toHaveBeenCalled();
150 warnSpy.mockRestore();
151 });
152
153 // Both rest_user_invalid_id (non-existent user) and rest_no_route (REST
154 // route disabled) collapse to the same user-facing error string.
155 it.each([["rest_user_invalid_id"], ["rest_no_route"]])(
156 "maps %s to a 'User not found' error",
157 async (code) => {
158 const errorSpy = jest
159 .spyOn(console, "error")
160 .mockImplementation(() => {});
161
162 const err = new Error("nope");
163 err.code = code;
164 apiFetch.mockRejectedValueOnce(err);
165
166 const { result, waitForNextUpdate } = renderHook(() =>
167 useUserDetail(1)
168 );
169 await waitForNextUpdate();
170 await settle();
171
172 expect(result.current.error).toBe("User not found");
173 errorSpy.mockRestore();
174 }
175 );
176
177 it("surfaces other errors verbatim", async () => {
178 const errorSpy = jest
179 .spyOn(console, "error")
180 .mockImplementation(() => {});
181
182 apiFetch.mockRejectedValueOnce(new Error("server down"));
183
184 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
185 await waitForNextUpdate();
186 await settle();
187
188 expect(result.current.error).toBe("server down");
189 errorSpy.mockRestore();
190 });
191
192 it("refetch(from, to) re-runs analytics with the new date range", async () => {
193 apiFetch
194 .mockResolvedValueOnce({ id: 1, name: "x" })
195 .mockResolvedValueOnce({ view: "0" })
196 .mockResolvedValueOnce({ view: "0" })
197 .mockResolvedValueOnce({ view: "0" });
198
199 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
200 await waitForNextUpdate();
201 await settle();
202
203 apiFetch.mockReset();
204 apiFetch
205 .mockResolvedValueOnce({ id: 1, name: "x" })
206 .mockResolvedValueOnce({ view: "0" })
207 .mockResolvedValueOnce({ view: "0" })
208 .mockResolvedValueOnce({ view: "0" });
209
210 await act(async () => {
211 result.current.refetch(
212 new Date(Date.UTC(2026, 0, 5)),
213 new Date(Date.UTC(2026, 0, 10))
214 );
215 });
216 await settle();
217
218 const analyticsPaths = apiFetch.mock.calls
219 .slice(1)
220 .map((c) => c[0].path);
221 expect(analyticsPaths.length).toBeGreaterThan(0);
222 expect(analyticsPaths.every((p) => p.includes("start=2026-01-05"))).toBe(
223 true
224 );
225 expect(analyticsPaths.every((p) => p.includes("end=2026-01-10"))).toBe(
226 true
227 );
228 });
229
230 it("refetch() with no args restores the default analytics range", async () => {
231 // First mount with the default analytics range.
232 apiFetch
233 .mockResolvedValueOnce({ id: 1, name: "x" })
234 .mockResolvedValueOnce({ view: "0" })
235 .mockResolvedValueOnce({ view: "0" })
236 .mockResolvedValueOnce({ view: "0" });
237
238 const { result, waitForNextUpdate } = renderHook(() => useUserDetail(1));
239 await waitForNextUpdate();
240 await settle();
241
242 // Switch to a custom range so a follow-up refetch() actually changes
243 // dateRange (going from null → null wouldn't trigger a re-fetch).
244 apiFetch.mockReset();
245 apiFetch
246 .mockResolvedValueOnce({ id: 1, name: "x" })
247 .mockResolvedValueOnce({ view: "0" })
248 .mockResolvedValueOnce({ view: "0" })
249 .mockResolvedValueOnce({ view: "0" });
250
251 await act(async () => {
252 result.current.refetch(
253 new Date(Date.UTC(2026, 0, 5)),
254 new Date(Date.UTC(2026, 0, 10))
255 );
256 });
257 await settle();
258
259 apiFetch.mockReset();
260 apiFetch
261 .mockResolvedValueOnce({ id: 1, name: "x" })
262 .mockResolvedValueOnce({ view: "0" })
263 .mockResolvedValueOnce({ view: "0" })
264 .mockResolvedValueOnce({ view: "0" });
265
266 await act(async () => {
267 result.current.refetch();
268 });
269 await settle();
270
271 // After clearing back to the default (DEFAULT_ANALYTICS_DAYS = 90),
272 // the start param should be the window's lower bound — frozen "today"
273 // 2026-05-10 minus 89 days = 2026-02-10 — not the all-time 2020-01-01
274 // sentinel. If DEFAULT_ANALYTICS_DAYS changes, recompute this date.
275 expect(apiFetch.mock.calls.length).toBeGreaterThanOrEqual(2);
276 const analyticsPath = apiFetch.mock.calls[1][0].path;
277 expect(analyticsPath).not.toContain("start=2020-01-01");
278 expect(analyticsPath).toContain("start=2026-02-10");
279 });
280 });
281