| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | decodeEventStream, |
| 5 | encodeEventStream, |
| 6 | Root, |
| 7 | type StreamNode, |
| 8 | type StreamEvent, |
| 9 | } from "./progress.ts"; |
| 10 | |
| 11 | describe("progress transport", () => { |
| 12 | it("reconstructs a late snapshot with nested nodes and empty labels", async () => { |
| 13 | const source = new Root(); |
| 14 | const parent = source.start("parent", { total: 8, value: 3 }); |
| 15 | parent.start("", { total: 4, value: 2 }); |
| 16 | const target = new Root(); |
| 17 | const input = encodeEventStream(source, { throttleMs: 0 }); |
| 18 | const decoding = decodeEventStream(input, target); |
| 19 | await new Promise<void>((resolve) => |
| 20 | target.on("node-start", () => { |
| 21 | if (target.active[0]?.children.length) resolve(); |
| 22 | }), |
| 23 | ); |
| 24 | expect(target.active[0]?.text).toBe("parent"); |
| 25 | expect(target.active[0]?.value).toBe(3); |
| 26 | expect(target.active[0]?.children[0]?.text).toBe(""); |
| 27 | expect(target.active[0]?.children[0]?.value).toBe(2); |
| 28 | decoding.cancel(); |
| 29 | expect(target.active).toHaveLength(0); |
| 30 | }); |
| 31 | |
| 32 | it("applies zero values, cleared text, and a cleared estimate", async () => { |
| 33 | const target = new Root(); |
| 34 | let controller: ReadableStreamDefaultController<StreamEvent>; |
| 35 | const input = new ReadableStream<StreamEvent>({ |
| 36 | start(value) { |
| 37 | controller = value; |
| 38 | }, |
| 39 | }); |
| 40 | const decoding = decodeEventStream<void, { checkpoint: [] }>(input, target); |
| 41 | const checkpoint = () => new Promise<void>((resolve) => decoding.on("checkpoint", resolve)); |
| 42 | let ready = checkpoint(); |
| 43 | controller!.enqueue([ |
| 44 | -1, |
| 45 | { k: 1 as StreamNode["k"], t: "task", v: 5, e: 10, E: 2000 }, |
| 46 | [1], |
| 47 | ["checkpoint"], |
| 48 | ]); |
| 49 | await ready; |
| 50 | ready = checkpoint(); |
| 51 | controller!.enqueue([ |
| 52 | 0, |
| 53 | { k: 1 as StreamNode["k"], t: "", v: 0, e: 0, E: null }, |
| 54 | ["checkpoint"], |
| 55 | ]); |
| 56 | await ready; |
| 57 | expect(target.active[0]).toMatchObject({ text: "", value: 0, total: 0, estimatedTime: null }); |
| 58 | controller!.enqueue([0, ["end", null]]); |
| 59 | controller!.close(); |
| 60 | await decoding; |
| 61 | expect(target.active).toHaveLength(0); |
| 62 | }); |
| 63 | |
| 64 | it("cancels a pending read and removes managed nodes without ending the transport", async () => { |
| 65 | const target = new Root(); |
| 66 | const cancelled = Promise.withResolvers<void>(); |
| 67 | let controller: ReadableStreamDefaultController<StreamEvent>; |
| 68 | const input = new ReadableStream<StreamEvent>({ |
| 69 | start(value) { |
| 70 | controller = value; |
| 71 | }, |
| 72 | cancel() { |
| 73 | cancelled.resolve(); |
| 74 | }, |
| 75 | }); |
| 76 | const decoding = decodeEventStream<void, { checkpoint: [] }>(input, target); |
| 77 | const ready = new Promise<void>((resolve) => decoding.on("checkpoint", resolve)); |
| 78 | controller!.enqueue([-1, { k: 1 as StreamNode["k"], t: "interrupted" }, [1], ["checkpoint"]]); |
| 79 | await ready; |
| 80 | decoding.cancel(); |
| 81 | await cancelled.promise; |
| 82 | expect(target.active).toHaveLength(0); |
| 83 | expect(input.locked).toBe(false); |
| 84 | }); |
| 85 | }); |