clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / package / runtime / boot-progress_test.ts
boot-progress_test.ts 3.5K
1import { createBootProgressTracker } from "./boot-progress.ts";23function assert(condition: unknown, message: string): asserts condition {4 if (!condition) throw new Error(message);5}67function streamedResponse(chunks: readonly Uint8Array[]): Response {8 let index = 0;9 return new Response(10 new ReadableStream<Uint8Array>({11 pull(controller) {12 if (index < chunks.length) controller.enqueue(chunks[index++]);13 else controller.close();14 },15 }),16 );17}1819Deno.test("engine warm-up reports byte progress through the declared size", async () => {20 const tracker = createBootProgressTracker({21 engineAsset: () => ({ url: "https://example.com/app/_astro/jazz.wasm", bytes: 100 }),22 fetchImpl: () => Promise.resolve(streamedResponse([new Uint8Array(40), new Uint8Array(60)])),23 });24 const observed: Array<{ phase: string; loadedBytes: number; totalBytes: number | null }> = [];25 tracker.subscribe((progress) => observed.push({ ...progress }));26 assert(tracker.get().phase === "pending", "tracker must start pending");27 await tracker.warmEngineDownload();28 assert(29 observed.some((entry) =>30 entry.phase === "downloading" && entry.loadedBytes === 0 && entry.totalBytes === 10031 ),32 "download start was not published with the declared size",33 );34 assert(35 tracker.get().phase === "downloading" && tracker.get().loadedBytes === 100,36 `download completion was not published: ${JSON.stringify(tracker.get())}`,37 );38 const loadedSequence = observed.map((entry) => entry.loadedBytes);39 assert(40 loadedSequence.some((bytes) => bytes === 40),41 `intermediate progress was not published: ${JSON.stringify(loadedSequence)}`,42 );43 tracker.mark("opening");44 tracker.mark("ready");45 assert(46 tracker.get().phase === "ready" && tracker.get().loadedBytes === 100,47 "phase transitions must preserve byte progress",48 );49});5051Deno.test("a shell without an engine declaration skips the download phase", async () => {52 const tracker = createBootProgressTracker({53 engineAsset: () => null,54 fetchImpl: () => {55 throw new Error("no declaration must mean no fetch");56 },57 });58 await tracker.warmEngineDownload();59 assert(tracker.get().phase === "pending", "phase must stay pending without a declaration");60 tracker.mark("opening");61 tracker.mark("ready");62 assert(tracker.get().phase === "ready", "lifecycle marks must still apply");63});6465Deno.test("warm-up failures resolve silently and run once per document", async () => {66 let fetches = 0;67 const tracker = createBootProgressTracker({68 engineAsset: () => ({ url: "https://example.com/app/_astro/jazz.wasm", bytes: null }),69 fetchImpl: () => {70 fetches += 1;71 return Promise.reject(new Error("offline"));72 },73 });74 await tracker.warmEngineDownload();75 await tracker.warmEngineDownload();76 assert(fetches === 1, `warm-up must be memoized per document, saw ${fetches} fetches`);77 assert(78 tracker.get().phase === "downloading" && tracker.get().totalBytes === null,79 "an undeclared size must publish a null total",80 );81});8283Deno.test("a non-OK engine response leaves resolution to the engine", async () => {84 const tracker = createBootProgressTracker({85 engineAsset: () => ({ url: "https://example.com/app/_astro/jazz.wasm", bytes: 100 }),86 fetchImpl: () => Promise.resolve(new Response("missing", { status: 404 })),87 });88 await tracker.warmEngineDownload();89 assert(90 tracker.get().loadedBytes === 0,91 "a failed download must not report progress",92 );93});