Skip to content

Commit d150e4f

Browse files
Add test for AbortSignal listener clean up
1 parent 49ef634 commit d150e4f

File tree

1 file changed

+47
-10
lines changed

1 file changed

+47
-10
lines changed

test/abort.ts

+47-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import test from 'ava';
55
import delay from 'delay';
66
import {pEvent} from 'p-event';
77
import type {Handler} from 'express';
8+
import {createSandbox} from 'sinon';
89
import got from '../source/index.js';
910
import slowDataStream from './helpers/slow-data-stream.js';
1011
import type {GlobalClock} from './helpers/types.js';
@@ -64,9 +65,25 @@ if (globalThis.AbortController !== undefined) {
6465
);
6566
};
6667

68+
const sandbox = createSandbox();
69+
70+
const createAbortController = (): {controller: AbortController; signalHandlersRemoved: () => boolean} => {
71+
const controller = new AbortController();
72+
sandbox.spy(controller.signal);
73+
// @ts-expect-error AbortSignal type definition issue: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/57805
74+
const signalHandlersRemoved = () => controller.signal.addEventListener.callCount === controller.signal.removeEventListener.callCount;
75+
return {
76+
controller, signalHandlersRemoved,
77+
};
78+
};
79+
80+
test.afterEach(() => {
81+
sandbox.restore();
82+
});
83+
6784
test.serial('does not retry after abort', withServerAndFakeTimers, async (t, server, got, clock) => {
6885
const {emitter, promise} = prepareServer(server, clock);
69-
const controller = new AbortController();
86+
const {controller, signalHandlersRemoved} = createAbortController();
7087

7188
const gotPromise = got('redirect', {
7289
signal: controller.signal,
@@ -88,12 +105,14 @@ if (globalThis.AbortController !== undefined) {
88105
});
89106

90107
await t.notThrowsAsync(promise, 'Request finished instead of aborting.');
108+
109+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
91110
});
92111

93112
test.serial('abort request timeouts', withServer, async (t, server, got) => {
94113
server.get('/', () => {});
95114

96-
const controller = new AbortController();
115+
const {controller, signalHandlersRemoved} = createAbortController();
97116

98117
const gotPromise = got({
99118
signal: controller.signal,
@@ -121,14 +140,16 @@ if (globalThis.AbortController !== undefined) {
121140
message: 'This operation was aborted.',
122141
});
123142

143+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
144+
124145
// Wait for unhandled errors
125146
await delay(40);
126147
});
127148

128149
test.serial('aborts in-progress request', withServerAndFakeTimers, async (t, server, got, clock) => {
129150
const {emitter, promise} = prepareServer(server, clock);
130151

131-
const controller = new AbortController();
152+
const {controller, signalHandlersRemoved} = createAbortController();
132153

133154
const body = new ReadableStream({
134155
read() {},
@@ -148,12 +169,14 @@ if (globalThis.AbortController !== undefined) {
148169
message: 'This operation was aborted.',
149170
});
150171
await t.notThrowsAsync(promise, 'Request finished instead of aborting.');
172+
173+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
151174
});
152175

153176
test.serial('aborts in-progress request with timeout', withServerAndFakeTimers, async (t, server, got, clock) => {
154177
const {emitter, promise} = prepareServer(server, clock);
155178

156-
const controller = new AbortController();
179+
const {controller, signalHandlersRemoved} = createAbortController();
157180

158181
const body = new ReadableStream({
159182
read() {},
@@ -173,10 +196,12 @@ if (globalThis.AbortController !== undefined) {
173196
message: 'This operation was aborted.',
174197
});
175198
await t.notThrowsAsync(promise, 'Request finished instead of aborting.');
199+
200+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
176201
});
177202

178203
test.serial('abort immediately', withServerAndFakeTimers, async (t, server, got, clock) => {
179-
const controller = new AbortController();
204+
const {controller, signalHandlersRemoved} = createAbortController();
180205

181206
const promise = new Promise<void>((resolve, reject) => {
182207
// We won't get an abort or even a connection
@@ -198,11 +223,13 @@ if (globalThis.AbortController !== undefined) {
198223
message: 'This operation was aborted.',
199224
});
200225
await t.notThrowsAsync(promise, 'Request finished instead of aborting.');
226+
227+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
201228
});
202229

203230
test('recover from abort using abortable promise attribute', async t => {
204231
// Abort before connection started
205-
const controller = new AbortController();
232+
const {controller, signalHandlersRemoved} = createAbortController();
206233

207234
const p = got('http://example.com', {signal: controller.signal});
208235
const recover = p.catch((error: Error) => {
@@ -216,10 +243,12 @@ if (globalThis.AbortController !== undefined) {
216243
controller.abort();
217244

218245
await t.notThrowsAsync(recover);
246+
247+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
219248
});
220249

221250
test('recover from abort using error instance', async t => {
222-
const controller = new AbortController();
251+
const {controller, signalHandlersRemoved} = createAbortController();
223252

224253
const p = got('http://example.com', {signal: controller.signal});
225254
const recover = p.catch((error: Error) => {
@@ -233,13 +262,15 @@ if (globalThis.AbortController !== undefined) {
233262
controller.abort();
234263

235264
await t.notThrowsAsync(recover);
265+
266+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
236267
});
237268

238269
// TODO: Use `fakeTimers` here
239270
test.serial('throws on incomplete (aborted) response', withServer, async (t, server, got) => {
240271
server.get('/', downloadHandler());
241272

242-
const controller = new AbortController();
273+
const {controller, signalHandlersRemoved} = createAbortController();
243274

244275
const promise = got('', {signal: controller.signal});
245276

@@ -251,6 +282,8 @@ if (globalThis.AbortController !== undefined) {
251282
code: 'ERR_ABORTED',
252283
message: 'This operation was aborted.',
253284
});
285+
286+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
254287
});
255288

256289
test('throws when aborting cached request', withServer, async (t, server, got) => {
@@ -263,18 +296,20 @@ if (globalThis.AbortController !== undefined) {
263296

264297
await got({cache});
265298

266-
const controller = new AbortController();
299+
const {controller, signalHandlersRemoved} = createAbortController();
267300
const promise = got({cache, signal: controller.signal});
268301
controller.abort();
269302

270303
await t.throwsAsync(promise, {
271304
code: 'ERR_ABORTED',
272305
message: 'This operation was aborted.',
273306
});
307+
308+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
274309
});
275310

276311
test('support setting the signal as a default option', async t => {
277-
const controller = new AbortController();
312+
const {controller, signalHandlersRemoved} = createAbortController();
278313

279314
const got2 = got.extend({signal: controller.signal});
280315
const p = got2('http://example.com', {signal: controller.signal});
@@ -284,6 +319,8 @@ if (globalThis.AbortController !== undefined) {
284319
code: 'ERR_ABORTED',
285320
message: 'This operation was aborted.',
286321
});
322+
323+
t.true(signalHandlersRemoved(), 'Abort signal event handlers not removed');
287324
});
288325
} else {
289326
test('x', t => {

0 commit comments

Comments
 (0)