Skip to content
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions test/js/web/abort/abort-signal-any.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { describe, expect, test } from "bun:test";

/**
* Comprehensive tests for AbortSignal.any()
*
* AbortSignal.any() creates a composite signal that aborts when any of the
* provided signals abort. This test suite covers edge cases and ensures
* standards-compliant behavior.
*/
describe("AbortSignal.any()", () => {
describe("basic functionality", () => {
test("should return a non-aborted signal for empty array", () => {
// @ts-ignore - TypeScript may not have this typed
const signal = AbortSignal.any([]);
expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);
});

test("should follow a single signal", () => {
const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);

expect(composite.aborted).toBe(false);
controller.abort();
expect(composite.aborted).toBe(true);
});

test("should abort when any of multiple signals abort", () => {
const controller1 = new AbortController();
const controller2 = new AbortController();
const controller3 = new AbortController();

// @ts-ignore
const composite = AbortSignal.any([
controller1.signal,
controller2.signal,
controller3.signal
]);

expect(composite.aborted).toBe(false);
controller2.abort("middle signal aborted");
expect(composite.aborted).toBe(true);
});
});

describe("already-aborted signals", () => {
test("should immediately abort if any input signal is already aborted", () => {
const abortedController = new AbortController();
abortedController.abort("pre-aborted");

const freshController = new AbortController();

// @ts-ignore
const composite = AbortSignal.any([
freshController.signal,
abortedController.signal
]);

expect(composite.aborted).toBe(true);
});

test("should use AbortSignal.abort() result correctly", () => {
const alreadyAborted = AbortSignal.abort("already aborted reason");
const controller = new AbortController();

// @ts-ignore
const composite = AbortSignal.any([controller.signal, alreadyAborted]);

expect(composite.aborted).toBe(true);
expect(composite.reason).toBe("already aborted reason");
});

test("should work with all signals already aborted", () => {
const aborted1 = AbortSignal.abort("first");
const aborted2 = AbortSignal.abort("second");

// @ts-ignore
const composite = AbortSignal.any([aborted1, aborted2]);

expect(composite.aborted).toBe(true);
// First aborted signal's reason should be used
expect(composite.reason).toBe("first");
});
});

describe("reason propagation", () => {
test("should propagate the reason from the aborting signal", () => {
const controller1 = new AbortController();
const controller2 = new AbortController();

// @ts-ignore
const composite = AbortSignal.any([controller1.signal, controller2.signal]);

const customReason = new Error("custom abort reason");
controller1.abort(customReason);

expect(composite.reason).toBe(customReason);
});

test("should use DOMException for default abort reason", () => {
const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);

controller.abort();

expect(composite.reason).toBeInstanceOf(DOMException);
expect(composite.reason.name).toBe("AbortError");
});

test("should propagate string reasons", () => {
const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);

controller.abort("string reason");

expect(composite.reason).toBe("string reason");
});

test("should propagate object reasons", () => {
const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);

const objectReason = { code: 42, message: "custom object" };
controller.abort(objectReason);

expect(composite.reason).toBe(objectReason);
});
});
Comment on lines 87 to 113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: consolidate reason propagation tests with test.each

The string, object, and custom-error reason tests all follow the same structure. You could reduce boilerplate and make it easier to add more cases later by using a table-driven test.each:

const cases = [
  { name: "Error", reason: new Error("custom abort reason") },
  { name: "string", reason: "string reason" },
  { name: "object", reason: { code: 42, message: "custom object" } },
];

test.each(cases)("should propagate %s reasons", ({ reason }) => {
  const controller = new AbortController();
  // @ts-ignore
  const composite = AbortSignal.any([controller.signal]);

  controller.abort(reason);
  expect(composite.reason).toBe(reason);
});

Purely stylistic; the current tests are fine functionally.

As per coding guidelines, using test.each for similar cases is encouraged.

🤖 Prompt for AI Agents
test/js/web/abort/abort-signal-any.test.ts lines 87-132: the three separate
tests that verify propagation of different abort reason types (Error, string,
object) are repetitive; replace them with a table-driven test using test.each
that enumerates the reason cases and runs the same setup/assertion for each case
(create AbortController, build composite via AbortSignal.any, call
controller.abort(reason), expect composite.reason toBe reason) so the test is
consolidated and easier to extend.


describe("event handling", () => {
test("should fire abort event when composite aborts", async () => {
const { promise, resolve } = Promise.withResolvers<boolean>();

const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);

composite.addEventListener("abort", () => resolve(true));

const timeout = setTimeout(() => resolve(false), 100);
controller.abort();

const result = await promise;
clearTimeout(timeout);
expect(result).toBe(true);
});

test("should only fire abort event once even with multiple source aborts", async () => {
let abortCount = 0;

const controller1 = new AbortController();
const controller2 = new AbortController();

// @ts-ignore
const composite = AbortSignal.any([controller1.signal, controller2.signal]);

composite.addEventListener("abort", () => abortCount++);

controller1.abort();
controller2.abort();

// Wait a tick to ensure all events have fired
await Bun.sleep(10);

expect(abortCount).toBe(1);
});
});

describe("nested AbortSignal.any()", () => {
test("should work with nested any() calls", () => {
const controller1 = new AbortController();
const controller2 = new AbortController();
const controller3 = new AbortController();

// @ts-ignore
const nested = AbortSignal.any([controller1.signal, controller2.signal]);
// @ts-ignore
const composite = AbortSignal.any([nested, controller3.signal]);

expect(composite.aborted).toBe(false);

controller2.abort("from nested");

expect(nested.aborted).toBe(true);
expect(composite.aborted).toBe(true);
expect(composite.reason).toBe("from nested");
});
});

describe("with AbortSignal.timeout()", () => {
test("should work with timeout signals", async () => {
const controller = new AbortController();
const timeoutSignal = AbortSignal.timeout(50);

// @ts-ignore
const composite = AbortSignal.any([controller.signal, timeoutSignal]);

expect(composite.aborted).toBe(false);

await Bun.sleep(100);

expect(composite.aborted).toBe(true);
expect(composite.reason).toBeInstanceOf(DOMException);
expect(composite.reason.name).toBe("TimeoutError");
});

test("should prefer manual abort over timeout if it comes first", async () => {
const controller = new AbortController();
const timeoutSignal = AbortSignal.timeout(1000);

// @ts-ignore
const composite = AbortSignal.any([controller.signal, timeoutSignal]);

controller.abort("manual abort");

expect(composite.aborted).toBe(true);
expect(composite.reason).toBe("manual abort");
});
});
});