Skip to content

Commit 157da12

Browse files
jonkaftonmbertrand
authored andcommitted
Refactor user lists and learning paths hooks out of learning resources (#1842)
* Refactor learning path API hooks out of learning resources * Refactor user list API hooks out of learning resources. Dead code commented out * Move post mutation helpers to hooks files * Remove empty file * Remove commented code
1 parent d688528 commit 157da12

File tree

20 files changed

+943
-1158
lines changed

20 files changed

+943
-1158
lines changed

frontends/api/src/clients.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const learningResourcesSearchAdminParamsApi =
5252

5353
const featuredApi = new FeaturedApi(undefined, BASE_PATH, axiosInstance)
5454

55-
const learningpathsApi = new LearningpathsApi(
55+
const learningPathsApi = new LearningpathsApi(
5656
undefined,
5757
BASE_PATH,
5858
axiosInstance,
@@ -95,7 +95,7 @@ const testimonialsApi = new TestimonialsApi(undefined, BASE_PATH, axiosInstance)
9595

9696
export {
9797
learningResourcesApi,
98-
learningpathsApi,
98+
learningPathsApi,
9999
userListsApi,
100100
topicsApi,
101101
articlesApi,
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { renderHook, waitFor } from "@testing-library/react"
2+
import { faker } from "@faker-js/faker/locale/en"
3+
import { UseQueryResult } from "@tanstack/react-query"
4+
import { LearningResource } from "../../generated/v1"
5+
import * as factories from "../../test-utils/factories"
6+
import { setupReactQueryTest } from "../test-utils"
7+
import { setMockResponse, urls, makeRequest } from "../../test-utils"
8+
import { useFeaturedLearningResourcesList } from "../learningResources"
9+
import { invalidateResourceQueries } from "../learningResources/invalidation"
10+
import keyFactory from "../learningResources/keyFactory"
11+
import {
12+
useLearningPathsDetail,
13+
useLearningPathsList,
14+
useInfiniteLearningPathItems,
15+
useLearningPathCreate,
16+
useLearningPathDestroy,
17+
useLearningPathUpdate,
18+
useLearningPathRelationshipMove,
19+
useLearningPathRelationshipCreate,
20+
useLearningPathRelationshipDestroy,
21+
} from "./index"
22+
import learningPathKeyFactory from "./keyFactory"
23+
24+
const factory = factories.learningResources
25+
26+
jest.mock("../learningResources/invalidation", () => {
27+
const actual = jest.requireActual("../learningResources/invalidation")
28+
return {
29+
__esModule: true,
30+
...actual,
31+
invalidateResourceQueries: jest.fn(),
32+
invalidateUserListQueries: jest.fn(),
33+
}
34+
})
35+
36+
/**
37+
* Assert that `hook` queries the API with the correct `url`, `method`, and
38+
* exposes the API's data.
39+
*/
40+
const assertApiCalled = async (
41+
result: { current: UseQueryResult },
42+
url: string,
43+
method: string,
44+
data: unknown,
45+
) => {
46+
await waitFor(() => expect(result.current.isLoading).toBe(false))
47+
expect(
48+
makeRequest.mock.calls.some((args) => {
49+
// Don't use toHaveBeenCalledWith. It doesn't handle undefined 3rd arg.
50+
return args[0].toUpperCase() === method && args[1] === url
51+
}),
52+
).toBe(true)
53+
expect(result.current.data).toEqual(data)
54+
}
55+
56+
describe("useLearningPathsList", () => {
57+
it.each([undefined, { limit: 5 }, { limit: 5, offset: 10 }])(
58+
"Calls the correct API",
59+
async (params) => {
60+
const data = factory.learningPaths({ count: 3 })
61+
const url = urls.learningPaths.list(params)
62+
63+
const { wrapper } = setupReactQueryTest()
64+
setMockResponse.get(url, data)
65+
const useTestHook = () => useLearningPathsList(params)
66+
const { result } = renderHook(useTestHook, { wrapper })
67+
68+
await assertApiCalled(result, url, "GET", data)
69+
},
70+
)
71+
})
72+
73+
describe("useInfiniteLearningPathItems", () => {
74+
it("Calls the correct API and can fetch next page", async () => {
75+
const parentId = faker.number.int()
76+
const url1 = urls.learningPaths.resources({
77+
learning_resource_id: parentId,
78+
})
79+
const url2 = urls.learningPaths.resources({
80+
learning_resource_id: parentId,
81+
offset: 5,
82+
})
83+
const response1 = factory.learningPathRelationships({
84+
count: 7,
85+
parent: parentId,
86+
next: url2,
87+
pageSize: 5,
88+
})
89+
const response2 = factory.learningPathRelationships({
90+
count: 7,
91+
pageSize: 2,
92+
parent: parentId,
93+
})
94+
setMockResponse.get(url1, response1)
95+
setMockResponse.get(url2, response2)
96+
const useTestHook = () =>
97+
useInfiniteLearningPathItems({ learning_resource_id: parentId })
98+
99+
const { wrapper } = setupReactQueryTest()
100+
101+
// First page
102+
const { result } = renderHook(useTestHook, { wrapper })
103+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
104+
expect(makeRequest).toHaveBeenCalledWith("get", url1, undefined)
105+
106+
// Second page
107+
result.current.fetchNextPage()
108+
await waitFor(() => expect(result.current.isFetching).toBe(false))
109+
expect(makeRequest).toHaveBeenCalledWith("get", url2, undefined)
110+
})
111+
})
112+
113+
describe("useLearningPathsRetrieve", () => {
114+
it("Calls the correct API", async () => {
115+
const data = factory.learningPath()
116+
const params = { id: data.id }
117+
const url = urls.learningPaths.details(params)
118+
119+
const { wrapper } = setupReactQueryTest()
120+
setMockResponse.get(url, data)
121+
const useTestHook = () => useLearningPathsDetail(params.id)
122+
const { result } = renderHook(useTestHook, { wrapper })
123+
124+
await assertApiCalled(result, url, "GET", data)
125+
})
126+
})
127+
128+
describe("LearningPath CRUD", () => {
129+
const makeData = () => {
130+
const path = factory.learningPath()
131+
const relationship = factory.learningPathRelationship({ parent: path.id })
132+
const keys = {
133+
learningResources: keyFactory._def,
134+
relationshipListing: learningPathKeyFactory.detail(path.id)._ctx
135+
.infiniteItems._def,
136+
}
137+
const pathUrls = {
138+
list: urls.learningPaths.list(),
139+
details: urls.learningPaths.details({ id: path.id }),
140+
relationshipList: urls.learningPaths.resources({
141+
learning_resource_id: path.id,
142+
}),
143+
relationshipDetails: urls.learningPaths.resourceDetails({
144+
id: relationship.id,
145+
learning_resource_id: path.id,
146+
}),
147+
}
148+
149+
const resourceWithoutList: LearningResource = {
150+
...relationship.resource,
151+
learning_path_parents:
152+
relationship.resource.learning_path_parents?.filter(
153+
(m) => m.id !== relationship.id,
154+
) ?? null,
155+
}
156+
return { path, relationship, pathUrls, keys, resourceWithoutList }
157+
}
158+
159+
test("useLearningPathCreate calls correct API", async () => {
160+
const { path, pathUrls } = makeData()
161+
const url = pathUrls.list
162+
163+
const requestData = { title: path.title }
164+
setMockResponse.post(pathUrls.list, path)
165+
166+
const { wrapper, queryClient } = setupReactQueryTest()
167+
jest.spyOn(queryClient, "invalidateQueries")
168+
const { result } = renderHook(useLearningPathCreate, {
169+
wrapper,
170+
})
171+
result.current.mutate(requestData)
172+
173+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
174+
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)
175+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
176+
"learningPaths",
177+
"list",
178+
])
179+
})
180+
181+
test("useLearningPathDestroy calls correct API", async () => {
182+
const { path, pathUrls } = makeData()
183+
const url = pathUrls.details
184+
setMockResponse.delete(url, null)
185+
186+
const { wrapper, queryClient } = setupReactQueryTest()
187+
const { result } = renderHook(useLearningPathDestroy, {
188+
wrapper,
189+
})
190+
result.current.mutate({ id: path.id })
191+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
192+
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
193+
expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)
194+
})
195+
196+
test("useLearningPathUpdate calls correct API", async () => {
197+
const { path, pathUrls } = makeData()
198+
const url = pathUrls.details
199+
const patch = { id: path.id, title: path.title }
200+
setMockResponse.patch(url, path)
201+
202+
const { wrapper, queryClient } = setupReactQueryTest()
203+
const { result } = renderHook(useLearningPathUpdate, { wrapper })
204+
result.current.mutate(patch)
205+
206+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
207+
expect(makeRequest).toHaveBeenCalledWith("patch", url, patch)
208+
209+
expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)
210+
})
211+
212+
test("useLearningPathRelationshipMove calls correct API", async () => {
213+
const { relationship, pathUrls, keys } = makeData()
214+
const url = pathUrls.relationshipDetails
215+
setMockResponse.patch(url, null)
216+
217+
const { wrapper, queryClient } = setupReactQueryTest()
218+
jest.spyOn(queryClient, "invalidateQueries")
219+
const { result } = renderHook(useLearningPathRelationshipMove, { wrapper })
220+
result.current.mutate(relationship)
221+
222+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
223+
expect(makeRequest).toHaveBeenCalledWith(
224+
"patch",
225+
url,
226+
expect.objectContaining({ position: relationship.position }),
227+
)
228+
229+
expect(queryClient.invalidateQueries).toHaveBeenCalledWith(
230+
keys.relationshipListing,
231+
)
232+
})
233+
234+
test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
235+
"useLearningPathRelationshipCreate calls correct API and patches featured resources",
236+
async ({ isChildFeatured }) => {
237+
const { relationship, pathUrls, resourceWithoutList } = makeData()
238+
239+
const featured = factory.resources({ count: 3 })
240+
if (isChildFeatured) {
241+
featured.results[0] = resourceWithoutList
242+
}
243+
setMockResponse.get(urls.learningResources.featured(), featured)
244+
245+
const url = pathUrls.relationshipList
246+
const requestData = {
247+
child: relationship.child,
248+
parent: relationship.parent,
249+
position: relationship.position,
250+
}
251+
setMockResponse.post(url, relationship)
252+
253+
const { wrapper, queryClient } = setupReactQueryTest()
254+
const { result } = renderHook(useLearningPathRelationshipCreate, {
255+
wrapper,
256+
})
257+
const { result: featuredResult } = renderHook(
258+
useFeaturedLearningResourcesList,
259+
{ wrapper },
260+
)
261+
await waitFor(() => expect(featuredResult.current.data).toBe(featured))
262+
263+
result.current.mutate(requestData)
264+
265+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
266+
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)
267+
268+
expect(invalidateResourceQueries).toHaveBeenCalledWith(
269+
queryClient,
270+
relationship.child,
271+
{ skipFeatured: false },
272+
)
273+
expect(invalidateResourceQueries).toHaveBeenCalledWith(
274+
queryClient,
275+
relationship.parent,
276+
)
277+
},
278+
)
279+
280+
test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
281+
"useLearningPathRelationshipDestroy calls correct API and patches child resource cache (isChildFeatured=$isChildFeatured)",
282+
async ({ isChildFeatured }) => {
283+
const { relationship, pathUrls } = makeData()
284+
const url = pathUrls.relationshipDetails
285+
286+
const featured = factory.resources({ count: 3 })
287+
if (isChildFeatured) {
288+
featured.results[0] = relationship.resource
289+
}
290+
setMockResponse.get(urls.learningResources.featured(), featured)
291+
292+
setMockResponse.delete(url, null)
293+
const { wrapper, queryClient } = setupReactQueryTest()
294+
295+
const { result } = renderHook(useLearningPathRelationshipDestroy, {
296+
wrapper,
297+
})
298+
const { result: featuredResult } = renderHook(
299+
useFeaturedLearningResourcesList,
300+
{ wrapper },
301+
)
302+
303+
await waitFor(() => expect(featuredResult.current.data).toBe(featured))
304+
result.current.mutate(relationship)
305+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
306+
307+
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
308+
expect(invalidateResourceQueries).toHaveBeenCalledWith(
309+
queryClient,
310+
relationship.child,
311+
{ skipFeatured: false },
312+
)
313+
expect(invalidateResourceQueries).toHaveBeenCalledWith(
314+
queryClient,
315+
relationship.parent,
316+
)
317+
},
318+
)
319+
})

0 commit comments

Comments
 (0)