AUDIT / src /lib /__tests__ /previewRuntime.test.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 2)
cc11e77 verified
Raw
History Blame Contribute Delete
11.7 kB
/**
* previewRuntime.test.ts — Unit test S28
*
* Testa PreviewRuntime:
* - setHtml: aggiorna stato + schedula auto-refresh (debounced)
* - refresh("manual"): refresh immediato + emit preview_refresh
* - captureError: aggiunge errore al buffer + emit preview_error
* - LRU buffer: non supera maxErrors
* - subscribe: notifica i listener ad ogni cambiamento
* - dispose: cancella timer + rimuove listener
* - createPreviewRuntime: factory genera ID unico
* - dedup HTML: setHtml con stesso valore non fa refresh
* - debounce: chiamate ravvicinate producono un solo refresh
*
* Mock: emitter via config.emitter (DI) — nessun vi.mock su moduli globali.
* Usa vi.useFakeTimers() per controllare il debounce senza race condition.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
PreviewRuntime,
createPreviewRuntime,
_setTestEmitter,
} from "@/lib/previewRuntime";
import type { EmitPayload } from "@/core/events/EventTypes";
// ─── Mock emitter ─────────────────────────────────────────────────────────────
function makeMockEmitter() {
const calls: EmitPayload[] = [];
const emitter = { emit: vi.fn((p: EmitPayload) => { calls.push(p); }) };
return { emitter, calls };
}
// ─── Fake timers ──────────────────────────────────────────────────────────────
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
// ─── PreviewRuntime ───────────────────────────────────────────────────────────
describe("PreviewRuntime — init", () => {
it("stato iniziale corretto", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
expect(rt.state.refreshKey).toBe(0);
expect(rt.state.errors).toHaveLength(0);
expect(rt.state.lastRefreshAt).toBe(0);
expect(rt.state.html).toBe("");
});
});
// ─── setHtml + auto-refresh debounce ─────────────────────────────────────────
describe("PreviewRuntime — setHtml", () => {
it("aggiorna html nello stato", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 200, emitter });
rt.setHtml("<h1>ciao</h1>");
expect(rt.state.html).toBe("<h1>ciao</h1>");
});
it("schedula auto-refresh dopo debounceMs", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 200, emitter });
rt.setHtml("<h1>test</h1>");
expect(rt.state.refreshKey).toBe(0); // non ancora refreshato
vi.advanceTimersByTime(200);
expect(rt.state.refreshKey).toBe(1);
expect(calls).toHaveLength(1);
expect(calls[0]?.kind).toBe("preview_refresh");
expect((calls[0] as { reason: string }).reason).toBe("html_change");
});
it("debounce: più setHtml ravvicinati → un solo refresh", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 200, emitter });
rt.setHtml("<h1>v1</h1>");
vi.advanceTimersByTime(100);
rt.setHtml("<h1>v2</h1>");
vi.advanceTimersByTime(100);
rt.setHtml("<h1>v3</h1>");
vi.advanceTimersByTime(200);
expect(rt.state.refreshKey).toBe(1);
expect(calls).toHaveLength(1);
});
it("nessun refresh se HTML è identico", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 200, emitter });
rt.setHtml("<h1>stesso</h1>");
vi.advanceTimersByTime(200);
const keyAfterFirst = rt.state.refreshKey;
rt.setHtml("<h1>stesso</h1>"); // identico
vi.advanceTimersByTime(200);
expect(rt.state.refreshKey).toBe(keyAfterFirst);
expect(calls).toHaveLength(1);
});
});
// ─── refresh manuale ──────────────────────────────────────────────────────────
describe("PreviewRuntime — refresh()", () => {
it("refresh manuale incrementa refreshKey + emette preview_refresh", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
rt.refresh("manual");
expect(rt.state.refreshKey).toBe(1);
expect(calls).toHaveLength(1);
expect(calls[0]?.kind).toBe("preview_refresh");
expect((calls[0] as { reason: string }).reason).toBe("manual");
});
it("refresh manuale cancella il debounce pendente", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 500, emitter });
rt.setHtml("<p>x</p>"); // schedula debounce 500ms
rt.refresh("manual"); // refresh immediato + cancella debounce
vi.advanceTimersByTime(500); // il debounce non dovrebbe sparare più
expect(rt.state.refreshKey).toBe(1); // solo 1 refresh
expect(calls).toHaveLength(1);
});
it("refresh svuota gli errori", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
rt.captureError("boom", "runtime");
expect(rt.state.errors).toHaveLength(1);
rt.refresh("manual");
expect(rt.state.errors).toHaveLength(0);
});
});
// ─── captureError ─────────────────────────────────────────────────────────────
describe("PreviewRuntime — captureError()", () => {
it("aggiunge errore al buffer + emette preview_error", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
rt.captureError("ReferenceError: x", "runtime", 42);
expect(rt.state.errors).toHaveLength(1);
expect(rt.state.errors[0]?.message).toBe("ReferenceError: x");
expect(rt.state.errors[0]?.source).toBe("runtime");
expect(rt.state.errors[0]?.line).toBe(42);
expect(calls[0]?.kind).toBe("preview_error");
expect((calls[0] as { previewId: string }).previewId).toBe("p1");
});
it("LRU: non supera maxErrors", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter, maxErrors: 3 });
rt.captureError("err1", "console");
rt.captureError("err2", "console");
rt.captureError("err3", "console");
rt.captureError("err4", "console");
expect(rt.state.errors).toHaveLength(3);
expect(rt.state.errors[0]?.message).toBe("err2");
expect(rt.state.errors[2]?.message).toBe("err4");
});
it("tronca messaggi lunghi a 500 chars", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
const long = "x".repeat(1000);
rt.captureError(long, "runtime");
expect(rt.state.errors[0]?.message).toHaveLength(500);
});
it("errorCount distingue runtime da console", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
rt.captureError("runtime err", "runtime");
rt.captureError("console err", "console");
rt.captureError("rejection", "unhandled_rejection");
expect(rt.errorCount).toBe(2); // runtime + unhandled_rejection
expect(rt.consoleErrorCount).toBe(1); // solo console
expect(rt.totalErrorCount).toBe(3);
});
});
// ─── subscribe ────────────────────────────────────────────────────────────────
describe("PreviewRuntime — subscribe()", () => {
it("notifica il listener dopo refresh", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
const snapshots: number[] = [];
rt.subscribe(s => snapshots.push(s.refreshKey));
rt.refresh("manual");
rt.refresh("manual");
expect(snapshots).toEqual([1, 2]);
});
it("unsubscribe funziona correttamente", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
const snapshots: number[] = [];
const unsub = rt.subscribe(s => snapshots.push(s.refreshKey));
rt.refresh("manual");
unsub();
rt.refresh("manual");
expect(snapshots).toEqual([1]); // solo il primo
});
it("notifica il listener dopo captureError", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
const counts: number[] = [];
rt.subscribe(s => counts.push(s.errors.length));
rt.captureError("err1", "console");
rt.captureError("err2", "runtime");
expect(counts).toEqual([1, 2]);
});
});
// ─── dispose ──────────────────────────────────────────────────────────────────
describe("PreviewRuntime — dispose()", () => {
it("cancella il debounce pendente", () => {
const { emitter, calls } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", debounceMs: 300, emitter });
rt.setHtml("<p>x</p>");
rt.dispose();
vi.advanceTimersByTime(300);
expect(calls).toHaveLength(0); // nessun refresh dopo dispose
});
it("rimuove tutti i listener", () => {
const { emitter } = makeMockEmitter();
const rt = new PreviewRuntime({ previewId: "p1", emitter });
const calls: number[] = [];
rt.subscribe(s => calls.push(s.refreshKey));
rt.dispose();
rt.refresh("manual"); // dopo dispose, non dovrebbe notificare
expect(calls).toHaveLength(0);
});
});
// ─── createPreviewRuntime factory ────────────────────────────────────────────
describe("createPreviewRuntime()", () => {
it("crea un'istanza con ID unico", () => {
const { emitter } = makeMockEmitter();
_setTestEmitter(emitter);
const rt1 = createPreviewRuntime();
const rt2 = createPreviewRuntime();
expect(rt1).not.toBe(rt2);
// Non esponiamo previewId pubblicamente — verifichiamo che gli emit abbiano ID diversi
rt1.captureError("e1", "runtime");
rt2.captureError("e2", "runtime");
const id1 = (emitter.emit.mock.calls[0]?.[0] as { previewId?: string })?.previewId;
const id2 = (emitter.emit.mock.calls[1]?.[0] as { previewId?: string })?.previewId;
expect(id1).toBeDefined();
expect(id2).toBeDefined();
expect(id1).not.toBe(id2);
rt1.dispose();
rt2.dispose();
});
});
// ─── _setTestEmitter ──────────────────────────────────────────────────────────
describe("_setTestEmitter()", () => {
it("sostituisce l'emitter di default", () => {
const { emitter, calls } = makeMockEmitter();
_setTestEmitter(emitter);
const rt = createPreviewRuntime(); // usa _defaultEmitter
rt.refresh("manual");
expect(calls).toHaveLength(1);
expect(calls[0]?.kind).toBe("preview_refresh");
rt.dispose();
});
});