Files
svg-tools/tests/optimization/optimizer-client.test.ts
T

130 lines
3.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
OptimizationCancelledError,
OptimizerClient,
} from "../../src/optimization/optimizer-client";
import type {
OptimizationRequest,
OptimizationResult,
} from "../../src/optimization/optimization.types";
class TestWorker extends EventTarget {
static instances: TestWorker[] = [];
readonly messages: unknown[] = [];
terminateCount = 0;
constructor() {
super();
TestWorker.instances.push(this);
}
postMessage(message: unknown): void {
this.messages.push(message);
}
terminate(): void {
this.terminateCount += 1;
}
respond(response: OptimizationResult): void {
this.dispatchEvent(new MessageEvent("message", { data: response }));
}
}
function resultFor(worker: TestWorker, source = "<svg/>"): OptimizationResult {
const request = worker.messages[0] as OptimizationRequest;
return {
type: "result",
jobId: request.jobId,
source,
profile: request.profile,
optionalPlugins: request.optionalPlugins,
inputBytes: source.length,
outputBytes: source.length,
elapsedMs: 1,
};
}
describe("OptimizerClient cancellation", () => {
beforeEach(() => {
TestWorker.instances = [];
vi.useFakeTimers();
vi.stubGlobal("Worker", TestWorker);
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it("rejects a cancelled job immediately and cleans up exactly once", async () => {
const client = new OptimizerClient();
const pending = client.optimize("<svg/>", "conservative");
const rejected = expect(pending).rejects.toBeInstanceOf(
OptimizationCancelledError,
);
const worker = TestWorker.instances[0]!;
expect(vi.getTimerCount()).toBe(1);
client.cancel();
client.cancel();
await rejected;
expect(worker.terminateCount).toBe(1);
expect(vi.getTimerCount()).toBe(0);
worker.respond(resultFor(worker, '<svg id="late"/>'));
expect(worker.terminateCount).toBe(1);
});
it("promptly rejects a superseded job while the replacement can finish", async () => {
const client = new OptimizerClient();
const first = client.optimize('<svg id="first"/>', "conservative");
const firstRejected = expect(first).rejects.toBeInstanceOf(
OptimizationCancelledError,
);
const firstWorker = TestWorker.instances[0]!;
const second = client.optimize('<svg id="second"/>', "standard");
const secondWorker = TestWorker.instances[1]!;
await firstRejected;
expect(firstWorker.terminateCount).toBe(1);
expect(secondWorker.terminateCount).toBe(0);
expect(vi.getTimerCount()).toBe(1);
const expected = resultFor(secondWorker, '<svg id="optimized"/>');
secondWorker.respond(expected);
await expect(second).resolves.toEqual(expected);
expect(secondWorker.terminateCount).toBe(1);
expect(vi.getTimerCount()).toBe(0);
client.cancel();
expect(secondWorker.terminateCount).toBe(1);
});
it("uses the same race-safe cleanup for AbortSignal cancellation", async () => {
const controller = new AbortController();
const client = new OptimizerClient();
const pending = client.optimize(
"<svg/>",
"aggressive",
[],
controller.signal,
);
const rejected = expect(pending).rejects.toBeInstanceOf(
OptimizationCancelledError,
);
const worker = TestWorker.instances[0]!;
controller.abort();
await rejected;
expect(worker.terminateCount).toBe(1);
expect(vi.getTimerCount()).toBe(0);
client.cancel();
expect(worker.terminateCount).toBe(1);
});
});