Skip to content
Closed
Changes from all commits
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
204 changes: 204 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,204 @@
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", () => {
// Table-driven tests for different reason types per coderabbitai suggestion
const reasonCases = [
{ name: "Error", reason: new Error("custom abort reason") },
{ name: "string", reason: "string reason" },
{ name: "object", reason: { code: 42, message: "custom object" } },
];

test.each(reasonCases)("should propagate $name reasons", ({ reason }) => {
const controller = new AbortController();
// @ts-ignore
const composite = AbortSignal.any([controller.signal]);
controller.abort(reason);
expect(composite.reason).toBe(reason);
});

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");
});
});
Comment on lines +87 to +113

Copy link
Copy Markdown
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));

// Abort fires synchronously, so the event listener is called before abort() returns
controller.abort();

const result = await promise;
expect(result).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

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");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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");
});
});
});