// @vitest-environment jsdom // // The queue as a tab watches it: a finish is a job that is done now and was not on the last look — // seen once, kept for the tray until it is closed, announced once. The first look only learns. import { beforeEach, describe, expect, it } from "vitest"; import type { QueueJob } from "./api"; import { clearFinished, forgetFinished, noteQueue, resetQueueWatch, takeUnannounced, uploadEnded, uploadStarted, useFinished, } from "./downloadQueue"; import { renderHook } from "@testing-library/react"; const job = (id: number, status: string) => ({ id, status }) as unknown as QueueJob; const upload = (id: number) => ({ id, status: "done", source_kind: "upload" }) as unknown as QueueJob; const finishedIds = () => renderHook(() => useFinished()).result.current.map((j) => j.id); beforeEach(resetQueueWatch); describe("downloadQueue · a finish", () => { it("the first look is old news: what is already done is not announced", () => { noteQueue([job(1, "done"), job(2, "running")]); expect(takeUnannounced()).toEqual([]); expect(finishedIds()).toEqual([]); }); it("a job that turns done is announced once and kept, newest first", () => { noteQueue([job(1, "running"), job(2, "queued")]); noteQueue([job(1, "done"), job(2, "running")]); noteQueue([job(1, "done"), job(2, "done")]); expect(takeUnannounced().map((j) => j.id)).toEqual([1, 2]); expect(takeUnannounced()).toEqual([]); expect(finishedIds()).toEqual([2, 1]); }); it("a job first seen done after the first look finished meanwhile (a cache hit, another device)", () => { noteQueue([]); noteQueue([job(5, "done")]); expect(takeUnannounced().map((j) => j.id)).toEqual([5]); }); it("a finish its own action announced is kept for the tray but not announced again", () => { noteQueue([]); uploadStarted(); uploadEnded(7); // an upload, done in the answer that made it noteQueue([upload(7), job(8, "done")]); expect(takeUnannounced().map((j) => j.id)).toEqual([8]); expect(finishedIds()).toEqual([7, 8]); }); it("an upload the queue sees done before its own answer arrives is still said once, by it", () => { noteQueue([]); uploadStarted(); noteQueue([upload(7)]); // the server committed it; the answer is still on its way expect(takeUnannounced()).toEqual([]); uploadEnded(7); expect(takeUnannounced()).toEqual([]); expect(finishedIds()).toEqual([7]); }); it("another device's upload landing meanwhile is announced once this tab's upload answers", () => { noteQueue([]); uploadStarted(); noteQueue([upload(7), upload(9)]); uploadEnded(7); expect(takeUnannounced().map((j) => j.id)).toEqual([9]); }); it("a download finishing during an upload is announced at once, not held for the upload", () => { noteQueue([]); uploadStarted(); noteQueue([job(8, "done")]); expect(takeUnannounced().map((j) => j.id)).toEqual([8]); uploadEnded(null); }); it("an upload that failed announced nothing: what landed meanwhile is announced", () => { noteQueue([]); uploadStarted(); noteQueue([upload(9)]); uploadEnded(null); expect(takeUnannounced().map((j) => j.id)).toEqual([9]); }); it("a failure is not a finish; closing the tray forgets what finished", () => { noteQueue([job(1, "running"), job(2, "running")]); noteQueue([job(1, "error"), job(2, "done")]); expect(finishedIds()).toEqual([2]); clearFinished(); expect(finishedIds()).toEqual([]); }); it("a finished file deleted since leaves the tray: its Open and Copy link would reach nothing", () => { noteQueue([job(1, "running"), job(2, "running")]); noteQueue([job(1, "done"), job(2, "done")]); forgetFinished(1); expect(finishedIds()).toEqual([2]); }); });