Spaces:
Sleeping
Sleeping
| /** | |
| * 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(); | |
| }); | |
| }); | |