|
| 1 | +// @vitest-environment happy-dom |
| 2 | + |
| 3 | +import React, { act } from "react"; |
| 4 | +import { createRoot, type Root } from "react-dom/client"; |
| 5 | +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 6 | +import { thumbnailScheduler } from "../player/lib/thumbnailScheduler"; |
| 7 | +import { useThumbnailStill } from "./useThumbnailStill"; |
| 8 | + |
| 9 | +Object.defineProperty(globalThis, "IS_REACT_ACT_ENVIRONMENT", { configurable: true, value: true }); |
| 10 | + |
| 11 | +class MockImage { |
| 12 | + onload: (() => void) | null = null; |
| 13 | + onerror: (() => void) | null = null; |
| 14 | + naturalWidth = 320; |
| 15 | + naturalHeight = 180; |
| 16 | + set src(_value: string) { |
| 17 | + queueMicrotask(() => this.onload?.()); |
| 18 | + } |
| 19 | +} |
| 20 | + |
| 21 | +const URL_ONE = "http://localhost:3000/api/projects/demo/thumbnail/index.html?t=1.00&revision=1"; |
| 22 | +const originalImage = globalThis.Image; |
| 23 | +const originalFetch = globalThis.fetch; |
| 24 | +const originalCreate = URL.createObjectURL; |
| 25 | +const originalRevoke = URL.revokeObjectURL; |
| 26 | +let host: HTMLDivElement; |
| 27 | +let root: Root | null = null; |
| 28 | +let shown: string | null = null; |
| 29 | + |
| 30 | +function Still({ url }: { url: string | null }) { |
| 31 | + shown = useThumbnailStill(url, "demo"); |
| 32 | + return null; |
| 33 | +} |
| 34 | + |
| 35 | +const mount = (url: string | null) => |
| 36 | + act(async () => { |
| 37 | + root ??= createRoot(host); |
| 38 | + root.render(React.createElement(Still, { url })); |
| 39 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 40 | + }); |
| 41 | + |
| 42 | +beforeEach(() => { |
| 43 | + globalThis.Image = MockImage as unknown as typeof Image; |
| 44 | + globalThis.fetch = vi.fn(async () => new Response(new Blob(["still"]), { status: 200 })); |
| 45 | + URL.createObjectURL = vi.fn(() => "blob:still"); |
| 46 | + URL.revokeObjectURL = vi.fn(); |
| 47 | + shown = null; |
| 48 | + host = document.createElement("div"); |
| 49 | + document.body.append(host); |
| 50 | +}); |
| 51 | + |
| 52 | +afterEach(() => { |
| 53 | + act(() => root?.unmount()); |
| 54 | + root = null; |
| 55 | + thumbnailScheduler.setPreviewReloading(false); |
| 56 | + thumbnailScheduler.invalidateProject("demo"); |
| 57 | + globalThis.Image = originalImage; |
| 58 | + globalThis.fetch = originalFetch; |
| 59 | + URL.createObjectURL = originalCreate; |
| 60 | + URL.revokeObjectURL = originalRevoke; |
| 61 | + document.body.replaceChildren(); |
| 62 | +}); |
| 63 | + |
| 64 | +describe("useThumbnailStill", () => { |
| 65 | + it("hands back the still's object URL once the scheduler has loaded it", async () => { |
| 66 | + await mount(URL_ONE); |
| 67 | + expect(globalThis.fetch).toHaveBeenCalledWith(URL_ONE, expect.anything()); |
| 68 | + expect(shown).toBe("blob:still"); |
| 69 | + }); |
| 70 | + |
| 71 | + it("asks for nothing without a url", async () => { |
| 72 | + await mount(null); |
| 73 | + expect(globalThis.fetch).not.toHaveBeenCalled(); |
| 74 | + expect(shown).toBeNull(); |
| 75 | + }); |
| 76 | + |
| 77 | + it("holds the fetch while a preview reloads, and runs it once the reload is over", async () => { |
| 78 | + thumbnailScheduler.setPreviewReloading(true); |
| 79 | + await mount(URL_ONE); |
| 80 | + expect(globalThis.fetch).not.toHaveBeenCalled(); |
| 81 | + expect(shown).toBeNull(); |
| 82 | + thumbnailScheduler.setPreviewReloading(false); |
| 83 | + await act(async () => { |
| 84 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 85 | + }); |
| 86 | + expect(globalThis.fetch).toHaveBeenCalledTimes(1); |
| 87 | + expect(shown).toBe("blob:still"); |
| 88 | + }); |
| 89 | +}); |
0 commit comments