mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-11 22:34:41 +00:00
133 lines
4.7 KiB
JavaScript
133 lines
4.7 KiB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
withRetries,
|
|
resetCodec2LoaderState,
|
|
ensureCodec2ScriptsLoaded,
|
|
startCodec2ScriptsBackgroundLoad,
|
|
} from "../../meshchatx/src/frontend/js/Codec2Loader.js";
|
|
|
|
describe("withRetries", () => {
|
|
it("succeeds on first attempt", async () => {
|
|
const fn = vi.fn().mockResolvedValue(undefined);
|
|
await withRetries(fn, { maxAttempts: 4, baseDelayMs: 1, maxDelayMs: 5 });
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("retries with backoff until success", async () => {
|
|
const fn = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error("a"))
|
|
.mockRejectedValueOnce(new Error("b"))
|
|
.mockResolvedValue(undefined);
|
|
await withRetries(fn, { maxAttempts: 6, baseDelayMs: 1, maxDelayMs: 5 });
|
|
expect(fn).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("throws after maxAttempts failures", async () => {
|
|
const fn = vi.fn().mockRejectedValue(new Error("always"));
|
|
await expect(withRetries(fn, { maxAttempts: 3, baseDelayMs: 1, maxDelayMs: 5 })).rejects.toThrow("always");
|
|
expect(fn).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("performance: completes quickly with tiny delays", async () => {
|
|
const fn = vi.fn().mockResolvedValue(undefined);
|
|
const t0 = performance.now();
|
|
await withRetries(fn, { maxAttempts: 8, baseDelayMs: 0, maxDelayMs: 1 });
|
|
expect(performance.now() - t0).toBeLessThan(500);
|
|
});
|
|
});
|
|
|
|
describe("Codec2Loader integration (jsdom)", () => {
|
|
const origAppend = document.head.appendChild.bind(document.head);
|
|
|
|
beforeEach(() => {
|
|
resetCodec2LoaderState();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
resetCodec2LoaderState();
|
|
document.head.appendChild = origAppend;
|
|
document.querySelectorAll("script[data-codec2-src]").forEach((el) => el.remove());
|
|
});
|
|
|
|
it("ensureCodec2ScriptsLoaded resolves when script tags load", async () => {
|
|
// Mock crypto.subtle.digest first
|
|
const mockHashBytes = new Uint8Array(48);
|
|
for (let i = 0; i < 48; i++) mockHashBytes[i] = i;
|
|
const mockHashB64 = btoa(String.fromCharCode(...mockHashBytes));
|
|
|
|
vi.stubGlobal("crypto", {
|
|
subtle: {
|
|
digest: vi.fn(async () => mockHashBytes.buffer),
|
|
},
|
|
});
|
|
|
|
// Mock fetch to return integrity.json and script content
|
|
const mockIntegrity = {
|
|
files: {
|
|
"c2enc.js": `sha384-${mockHashB64}`,
|
|
"c2dec.js": `sha384-${mockHashB64}`,
|
|
"sox.js": `sha384-${mockHashB64}`,
|
|
"codec2-lib.js": `sha384-${mockHashB64}`,
|
|
"wav-encoder.js": `sha384-${mockHashB64}`,
|
|
"codec2-microphone-recorder.js": `sha384-${mockHashB64}`,
|
|
},
|
|
};
|
|
|
|
vi.spyOn(globalThis, "fetch").mockImplementation((url) => {
|
|
if (url.includes("integrity.json")) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: async () => mockIntegrity,
|
|
});
|
|
}
|
|
// Return mock script content for any script request
|
|
const content = "// mock script";
|
|
return Promise.resolve({
|
|
ok: true,
|
|
arrayBuffer: async () => new TextEncoder().encode(content).buffer,
|
|
});
|
|
});
|
|
|
|
vi.spyOn(document.head, "appendChild").mockImplementation((node) => {
|
|
if (node instanceof HTMLScriptElement && node.src) {
|
|
origAppend(node);
|
|
queueMicrotask(() => node.dispatchEvent(new Event("load")));
|
|
return node;
|
|
}
|
|
return origAppend(node);
|
|
});
|
|
|
|
await ensureCodec2ScriptsLoaded();
|
|
await expect(ensureCodec2ScriptsLoaded()).resolves.toBeUndefined();
|
|
expect(document.querySelectorAll("script[data-codec2-src]").length).toBe(6);
|
|
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("startCodec2ScriptsBackgroundLoad swallows errors after retries", async () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
// Mock crypto
|
|
vi.stubGlobal("crypto", {
|
|
subtle: {
|
|
digest: vi.fn(async () => new ArrayBuffer(48)),
|
|
},
|
|
});
|
|
|
|
// Mock fetch to fail (simulate network/SRI failure)
|
|
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("network error"));
|
|
|
|
await startCodec2ScriptsBackgroundLoad({
|
|
maxAttempts: 2,
|
|
baseDelayMs: 0,
|
|
maxDelayMs: 1,
|
|
});
|
|
|
|
expect(warn).toHaveBeenCalled();
|
|
warn.mockRestore();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
});
|