Skip to content

Commit 47450df

Browse files
committed
Add tests for AbortController.
1 parent dabcb8b commit 47450df

File tree

4 files changed

+67
-74
lines changed

4 files changed

+67
-74
lines changed

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const createInstance = (defaultProps = {}, displayName = "Async") => {
5858
componentDidUpdate(prevProps) {
5959
if (prevProps.watch !== this.props.watch) this.load()
6060
if (prevProps.promiseFn !== this.props.promiseFn) {
61-
this.cancel()
6261
if (this.props.promiseFn) this.load()
62+
else this.cancel()
6363
}
6464
}
6565

src/spec.js

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import React from "react"
33
import { render, fireEvent, cleanup, waitForElement } from "react-testing-library"
44
import Async, { createInstance } from "./"
55

6-
const abortController = { abort: () => {} }
7-
window.AbortController = jest.fn().mockImplementation(() => abortController)
6+
const abortCtrl = { abort: jest.fn() }
7+
window.AbortController = jest.fn().mockImplementation(() => abortCtrl)
88

9+
beforeEach(abortCtrl.abort.mockClear)
910
afterEach(cleanup)
1011

1112
const resolveIn = ms => value => new Promise(resolve => setTimeout(resolve, ms, value))
@@ -23,7 +24,7 @@ describe("Async", () => {
2324
test("calls promiseFn with props", () => {
2425
const promiseFn = jest.fn().mockReturnValue(Promise.resolve())
2526
render(<Async promiseFn={promiseFn} anotherProp="123" />)
26-
expect(promiseFn).toHaveBeenCalledWith({ promiseFn, anotherProp: "123" }, abortController)
27+
expect(promiseFn).toHaveBeenCalledWith({ promiseFn, anotherProp: "123" }, abortCtrl)
2728
})
2829

2930
test("passes resolved data to children as render prop", async () => {
@@ -99,6 +100,7 @@ describe("Async", () => {
99100
expect(promiseFn).toHaveBeenCalledTimes(1)
100101
fireEvent.click(getByText("reload"))
101102
expect(promiseFn).toHaveBeenCalledTimes(2)
103+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
102104
})
103105

104106
test("re-runs the promise when the value of 'watch' changes", () => {
@@ -121,11 +123,13 @@ describe("Async", () => {
121123
expect(promiseFn).toHaveBeenCalledTimes(1)
122124
fireEvent.click(getByText("increment"))
123125
expect(promiseFn).toHaveBeenCalledTimes(2)
126+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
124127
fireEvent.click(getByText("increment"))
125128
expect(promiseFn).toHaveBeenCalledTimes(3)
129+
expect(abortCtrl.abort).toHaveBeenCalledTimes(2)
126130
})
127131

128-
test("runs deferFn only when explicitly invoked, passing arguments and props", () => {
132+
test("runs deferFn only when explicitly invoked, passing arguments, props and AbortController", () => {
129133
let counter = 1
130134
const deferFn = jest.fn().mockReturnValue(resolveTo())
131135
const { getByText } = render(
@@ -135,21 +139,12 @@ describe("Async", () => {
135139
}}
136140
</Async>
137141
)
142+
const props = { deferFn, foo: "bar" }
138143
expect(deferFn).not.toHaveBeenCalled()
139144
fireEvent.click(getByText("run"))
140-
expect(deferFn).toHaveBeenCalledWith(
141-
"go",
142-
1,
143-
expect.objectContaining({ deferFn, foo: "bar" }),
144-
abortController
145-
)
145+
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining(props), abortCtrl)
146146
fireEvent.click(getByText("run"))
147-
expect(deferFn).toHaveBeenCalledWith(
148-
"go",
149-
2,
150-
expect.objectContaining({ deferFn, foo: "bar" }),
151-
abortController
152-
)
147+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining(props), abortCtrl)
153148
})
154149

155150
test("reload uses the arguments of the previous run", () => {
@@ -169,26 +164,11 @@ describe("Async", () => {
169164
)
170165
expect(deferFn).not.toHaveBeenCalled()
171166
fireEvent.click(getByText("run"))
172-
expect(deferFn).toHaveBeenCalledWith(
173-
"go",
174-
1,
175-
expect.objectContaining({ deferFn }),
176-
abortController
177-
)
167+
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }), abortCtrl)
178168
fireEvent.click(getByText("run"))
179-
expect(deferFn).toHaveBeenCalledWith(
180-
"go",
181-
2,
182-
expect.objectContaining({ deferFn }),
183-
abortController
184-
)
169+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }), abortCtrl)
185170
fireEvent.click(getByText("reload"))
186-
expect(deferFn).toHaveBeenCalledWith(
187-
"go",
188-
2,
189-
expect.objectContaining({ deferFn }),
190-
abortController
191-
)
171+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }), abortCtrl)
192172
})
193173

194174
test("only accepts the last invocation of the promise", async () => {
@@ -236,6 +216,7 @@ describe("Async", () => {
236216
unmount()
237217
await Promise.resolve()
238218
expect(onResolve).not.toHaveBeenCalled()
219+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
239220
})
240221

241222
test("cancels and restarts the promise when promiseFn changes", async () => {
@@ -247,6 +228,17 @@ describe("Async", () => {
247228
await Promise.resolve()
248229
expect(onResolve).not.toHaveBeenCalledWith("one")
249230
expect(onResolve).toHaveBeenCalledWith("two")
231+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
232+
})
233+
234+
test("cancels the promise when promiseFn is unset", async () => {
235+
const promiseFn = jest.fn().mockReturnValue(Promise.resolve("one"))
236+
const onResolve = jest.fn()
237+
const { rerender } = render(<Async promiseFn={promiseFn} onResolve={onResolve} />)
238+
rerender(<Async onResolve={onResolve} />)
239+
await Promise.resolve()
240+
expect(onResolve).not.toHaveBeenCalledWith("one")
241+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
250242
})
251243

252244
test("does not run promiseFn on mount when initialValue is provided", () => {

src/useAsync.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,13 @@ const useAsync = (opts, init) => {
7070
}
7171
}
7272

73-
useEffect(() => load() && undefined, [promiseFn, watch])
73+
const cancel = () => {
74+
counter.current++
75+
abortController.current.abort()
76+
setState(state => ({ ...state, startedAt: undefined }))
77+
}
78+
79+
useEffect(() => (promiseFn ? load() && undefined : cancel()), [promiseFn, watch])
7480
useEffect(() => () => (isMounted.current = false), [])
7581
useEffect(() => abortController.current.abort, [])
7682

@@ -81,11 +87,7 @@ const useAsync = (opts, init) => {
8187
initialValue,
8288
run,
8389
reload: () => (lastArgs.current ? run(...lastArgs.current) : load()),
84-
cancel: () => {
85-
counter.current++
86-
abortController.current.abort()
87-
setState(state => ({ ...state, startedAt: undefined }))
88-
},
90+
cancel,
8991
setData: handleData,
9092
setError: handleError,
9193
}),

src/useAsync.spec.js

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import React from "react"
33
import { render, fireEvent, cleanup, waitForElement, flushEffects } from "react-testing-library"
44
import { useAsync } from "."
55

6-
const abortController = { abort: () => {} }
7-
window.AbortController = jest.fn().mockImplementation(() => abortController)
6+
const abortCtrl = { abort: jest.fn() }
7+
window.AbortController = jest.fn().mockImplementation(() => abortCtrl)
88

9+
beforeEach(abortCtrl.abort.mockClear)
910
afterEach(cleanup)
1011

1112
const resolveIn = ms => value => new Promise(resolve => setTimeout(resolve, ms, value))
@@ -95,6 +96,7 @@ describe("useAsync", () => {
9596
expect(promiseFn).toHaveBeenCalledTimes(1)
9697
fireEvent.click(getByText("reload"))
9798
expect(promiseFn).toHaveBeenCalledTimes(2)
99+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
98100
})
99101

100102
test("re-runs the promise when the value of 'watch' changes", () => {
@@ -118,9 +120,11 @@ describe("useAsync", () => {
118120
fireEvent.click(getByText("increment"))
119121
flushEffects()
120122
expect(promiseFn).toHaveBeenCalledTimes(2)
123+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
121124
fireEvent.click(getByText("increment"))
122125
flushEffects()
123126
expect(promiseFn).toHaveBeenCalledTimes(3)
127+
expect(abortCtrl.abort).toHaveBeenCalledTimes(2)
124128
})
125129

126130
test("runs deferFn only when explicitly invoked, passing arguments and props", () => {
@@ -134,25 +138,16 @@ describe("useAsync", () => {
134138
</Async>
135139
)
136140
const { getByText } = render(component)
141+
const props = { deferFn, foo: "bar" }
137142
flushEffects()
138143
expect(deferFn).not.toHaveBeenCalled()
139144
fireEvent.click(getByText("run"))
140-
expect(deferFn).toHaveBeenCalledWith(
141-
"go",
142-
1,
143-
expect.objectContaining({ deferFn, foo: "bar" }),
144-
abortController
145-
)
145+
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining(props), abortCtrl)
146146
fireEvent.click(getByText("run"))
147-
expect(deferFn).toHaveBeenCalledWith(
148-
"go",
149-
2,
150-
expect.objectContaining({ deferFn, foo: "bar" }),
151-
abortController
152-
)
147+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining(props), abortCtrl)
153148
})
154149

155-
test("cancel will prevent the resolved promise from propagating", async () => {
150+
test("cancel will prevent the resolved promise from propagating and attempts to abort it", async () => {
156151
const promiseFn = jest.fn().mockReturnValue(Promise.resolve("ok"))
157152
const onResolve = jest.fn()
158153
const component = (
@@ -165,6 +160,7 @@ describe("useAsync", () => {
165160
fireEvent.click(getByText("cancel"))
166161
await Promise.resolve()
167162
expect(onResolve).not.toHaveBeenCalled()
163+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
168164
})
169165

170166
test("reload uses the arguments of the previous run", () => {
@@ -186,26 +182,11 @@ describe("useAsync", () => {
186182
flushEffects()
187183
expect(deferFn).not.toHaveBeenCalled()
188184
fireEvent.click(getByText("run"))
189-
expect(deferFn).toHaveBeenCalledWith(
190-
"go",
191-
1,
192-
expect.objectContaining({ deferFn }),
193-
abortController
194-
)
185+
expect(deferFn).toHaveBeenCalledWith("go", 1, expect.objectContaining({ deferFn }), abortCtrl)
195186
fireEvent.click(getByText("run"))
196-
expect(deferFn).toHaveBeenCalledWith(
197-
"go",
198-
2,
199-
expect.objectContaining({ deferFn }),
200-
abortController
201-
)
187+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }), abortCtrl)
202188
fireEvent.click(getByText("reload"))
203-
expect(deferFn).toHaveBeenCalledWith(
204-
"go",
205-
2,
206-
expect.objectContaining({ deferFn }),
207-
abortController
208-
)
189+
expect(deferFn).toHaveBeenCalledWith("go", 2, expect.objectContaining({ deferFn }), abortCtrl)
209190
})
210191

211192
test("only accepts the last invocation of the promise", async () => {
@@ -260,6 +241,7 @@ describe("useAsync", () => {
260241
unmount()
261242
await Promise.resolve()
262243
expect(onResolve).not.toHaveBeenCalled()
244+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
263245
})
264246

265247
test("cancels and restarts the promise when promiseFn changes", async () => {
@@ -278,6 +260,23 @@ describe("useAsync", () => {
278260
expect(onResolve).not.toHaveBeenCalledWith("one")
279261
await Promise.resolve()
280262
expect(onResolve).toHaveBeenCalledWith("two")
263+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
264+
})
265+
266+
test("cancels the promise when promiseFn is unset", async () => {
267+
const promiseFn = jest.fn().mockReturnValue(Promise.resolve("one"))
268+
const onResolve = jest.fn()
269+
const component1 = <Async promiseFn={promiseFn} onResolve={onResolve} />
270+
const component2 = <Async onResolve={onResolve} />
271+
const { rerender } = render(component1)
272+
await Promise.resolve()
273+
flushEffects()
274+
expect(promiseFn).toHaveBeenCalled()
275+
rerender(component2)
276+
flushEffects()
277+
expect(onResolve).not.toHaveBeenCalledWith("one")
278+
await Promise.resolve()
279+
expect(abortCtrl.abort).toHaveBeenCalledTimes(1)
281280
})
282281

283282
test("does not run promiseFn on mount when initialValue is provided", () => {

0 commit comments

Comments
 (0)