b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { runBulkAction } from "../../src/lib/bulk";
|
|
|
|
/** A deferred promise whose resolution the test controls. */
|
|
function deferred() {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((res) => {
|
|
resolve = res;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
describe("runBulkAction", () => {
|
|
it("runs every id and reports no failures on success", async () => {
|
|
const seen: string[] = [];
|
|
const result = await runBulkAction(["a", "b", "c"], async (id) => {
|
|
seen.push(id);
|
|
});
|
|
expect(seen.toSorted()).toEqual(["a", "b", "c"]);
|
|
expect(result.failedIds).toEqual([]);
|
|
});
|
|
|
|
it("collects failed ids in input order without aborting the rest", async () => {
|
|
const seen: string[] = [];
|
|
const result = await runBulkAction(["a", "b", "c", "d"], async (id) => {
|
|
seen.push(id);
|
|
if (id === "c" || id === "a") throw new Error(`boom ${id}`);
|
|
});
|
|
expect(seen.toSorted()).toEqual(["a", "b", "c", "d"]);
|
|
expect(result.failedIds).toEqual(["a", "c"]);
|
|
});
|
|
|
|
it("never exceeds the concurrency limit", async () => {
|
|
const limit = 2;
|
|
let inFlight = 0;
|
|
let maxInFlight = 0;
|
|
const gates = new Map<string, ReturnType<typeof deferred>>();
|
|
const ids = ["a", "b", "c", "d", "e"];
|
|
for (const id of ids) gates.set(id, deferred());
|
|
|
|
const run = runBulkAction(
|
|
ids,
|
|
async (id) => {
|
|
inFlight++;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
await gates.get(id)!.promise;
|
|
inFlight--;
|
|
},
|
|
limit,
|
|
);
|
|
|
|
// Release the gates one at a time so the queue has to refill.
|
|
for (const id of ids) {
|
|
// Let the queue start whatever it can before releasing the next gate.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
gates.get(id)!.resolve();
|
|
}
|
|
const result = await run;
|
|
|
|
expect(maxInFlight).toBeLessThanOrEqual(limit);
|
|
expect(result.failedIds).toEqual([]);
|
|
});
|
|
|
|
it("handles an empty id list", async () => {
|
|
const result = await runBulkAction([], () => Promise.resolve());
|
|
expect(result.failedIds).toEqual([]);
|
|
});
|
|
});
|