// Flags: --expose-gc 'use strict'; const common = require('../common'); const { inspect } = require('util'); const assert = require('assert'); const { test, mock, } = require('node:test'); const { setTimeout: sleep } = require('timers/promises'); // All of the tests in this file depend on public-facing Node.js APIs. // For tests that depend on Node.js internal APIs, please add them to // test-abortcontroller-internal.js instead. test('Abort is fired with the correct event type on AbortControllers', () => { // Tests that abort is fired with the correct event type on AbortControllers const ac = new AbortController(); assert.ok(ac.signal); const fn = mock.fn(common.mustCall((event) => { assert.ok(event); assert.strictEqual(event.type, 'abort'); }, 2)); ac.signal.onabort = fn; ac.signal.addEventListener('abort', fn); ac.abort(); ac.abort(); assert.ok(ac.signal.aborted); assert.strictEqual(fn.mock.calls.length, 2); }); test('Abort events are trusted', () => { // Tests that abort events are trusted const ac = new AbortController(); const fn = mock.fn(common.mustCall((event) => { assert.ok(event.isTrusted); })); ac.signal.onabort = fn; ac.abort(); assert.strictEqual(fn.mock.calls.length, 1); }); test('Abort events have the same isTrusted reference', () => { // Tests that abort events have the same `isTrusted` reference const first = new AbortController(); const second = new AbortController(); let ev1, ev2; const ev3 = new Event('abort'); first.signal.addEventListener('abort', (event) => { ev1 = event; }); second.signal.addEventListener('abort', (event) => { ev2 = event; }); first.abort(); second.abort(); const firstTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev1), 'isTrusted').get; const secondTrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev2), 'isTrusted').get; const untrusted = Reflect.getOwnPropertyDescriptor(Object.getPrototypeOf(ev3), 'isTrusted').get; assert.strictEqual(firstTrusted, secondTrusted); assert.strictEqual(untrusted, firstTrusted); }); test('AbortSignal is impossible to construct manually', () => { // Tests that AbortSignal is impossible to construct manually const ac = new AbortController(); assert.throws(() => new ac.signal.constructor(), { code: 'ERR_ILLEGAL_CONSTRUCTOR', }); }); test('Symbol.toStringTag is correct', () => { // Symbol.toStringTag const toString = (o) => Object.prototype.toString.call(o); const ac = new AbortController(); assert.strictEqual(toString(ac), '[object AbortController]'); assert.strictEqual(toString(ac.signal), '[object AbortSignal]'); }); test('AbortSignal.abort() creates an already aborted signal', () => { const signal = AbortSignal.abort(); assert.ok(signal.aborted); }); test('AbortController properties and methods validate the receiver', () => { const acSignalGet = Object.getOwnPropertyDescriptor( AbortController.prototype, 'signal' ).get; const acAbort = AbortController.prototype.abort; const goodController = new AbortController(); assert.ok(acSignalGet.call(goodController)); acAbort.call(goodController); const badAbortControllers = [ null, undefined, 0, NaN, true, 'AbortController', { __proto__: AbortController.prototype }, ]; for (const badController of badAbortControllers) { assert.throws( () => acSignalGet.call(badController), { name: 'TypeError' } ); assert.throws( () => acAbort.call(badController), { name: 'TypeError' } ); } }); test('AbortSignal properties validate the receiver', () => { const signalAbortedGet = Object.getOwnPropertyDescriptor( AbortSignal.prototype, 'aborted' ).get; const goodSignal = new AbortController().signal; assert.strictEqual(signalAbortedGet.call(goodSignal), false); const badAbortSignals = [ null, undefined, 0, NaN, true, 'AbortSignal', { __proto__: AbortSignal.prototype }, ]; for (const badSignal of badAbortSignals) { assert.throws( () => signalAbortedGet.call(badSignal), { name: 'TypeError' } ); } }); test('AbortController inspection depth 1 or null works', () => { const ac = new AbortController(); assert.strictEqual(inspect(ac, { depth: 1 }), 'AbortController { signal: [AbortSignal] }'); assert.strictEqual(inspect(ac, { depth: null }), 'AbortController { signal: AbortSignal { aborted: false } }'); }); test('AbortSignal reason is set correctly', () => { // Test AbortSignal.reason // eslint-disable-next-line node-core/prefer-abort-signal-abort const ac = new AbortController(); ac.abort('reason'); assert.strictEqual(ac.signal.reason, 'reason'); }); test('AbortSignal reasonable is set correctly with AbortSignal.abort()', () => { // Test AbortSignal.reason const signal = AbortSignal.abort('reason'); assert.strictEqual(signal.reason, 'reason'); }); test('AbortSignal.timeout() works as expected', async () => { // Test AbortSignal timeout const signal = AbortSignal.timeout(10); assert.ok(!signal.aborted); const { promise, resolve } = Promise.withResolvers(); const fn = mock.fn(common.mustCall(() => { assert.ok(signal.aborted); assert.strictEqual(signal.reason.name, 'TimeoutError'); assert.strictEqual(signal.reason.code, 23); resolve(); })); setTimeout(fn, 20); await promise; }); test('AbortSignal.timeout() does not prevent the signal from being collected', async () => { // Test AbortSignal timeout doesn't prevent the signal // from being garbage collected. let ref; { ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000)); } await sleep(10); globalThis.gc(); assert.strictEqual(ref.deref(), undefined); }); test('AbortSignal with a timeout is not collected while there is an active listener', async () => { // Test that an AbortSignal with a timeout is not gc'd while // there is an active listener on it. let ref; function handler() {} { ref = new globalThis.WeakRef(AbortSignal.timeout(1_200_000)); ref.deref().addEventListener('abort', handler); } await sleep(10); globalThis.gc(); assert.notStrictEqual(ref.deref(), undefined); assert.ok(ref.deref() instanceof AbortSignal); ref.deref().removeEventListener('abort', handler); await sleep(10); globalThis.gc(); assert.strictEqual(ref.deref(), undefined); }); test('Setting a long timeout should not keep the process open', () => { AbortSignal.timeout(1_200_000); }); test('AbortSignal.reason should default', () => { // Test AbortSignal.reason default const signal = AbortSignal.abort(); assert.ok(signal.reason instanceof DOMException); assert.strictEqual(signal.reason.code, 20); // eslint-disable-next-line node-core/prefer-abort-signal-abort const ac = new AbortController(); ac.abort(); assert.ok(ac.signal.reason instanceof DOMException); assert.strictEqual(ac.signal.reason.code, 20); }); test('abortSignal.throwIfAborted() works as expected', () => { // Test abortSignal.throwIfAborted() assert.throws(() => AbortSignal.abort().throwIfAborted(), { code: 20, name: 'AbortError', }); // Does not throw because it's not aborted. const ac = new AbortController(); ac.signal.throwIfAborted(); }); test('abortSignal.throwIfAobrted() works as expected (2)', () => { const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted'); const actualReason = new Error(); Reflect.defineProperty(AbortSignal.prototype, 'aborted', { value: false }); assert.throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason); Reflect.defineProperty(AbortSignal.prototype, 'aborted', originalDesc); }); test('abortSignal.throwIfAobrted() works as expected (3)', () => { const originalDesc = Reflect.getOwnPropertyDescriptor(AbortSignal.prototype, 'reason'); const actualReason = new Error(); const fakeExcuse = new Error(); Reflect.defineProperty(AbortSignal.prototype, 'reason', { value: fakeExcuse }); assert.throws(() => AbortSignal.abort(actualReason).throwIfAborted(), actualReason); Reflect.defineProperty(AbortSignal.prototype, 'reason', originalDesc); });