|
| 1 | +// @vitest-environment happy-dom |
| 2 | + |
| 3 | +import React, { act } from "react"; |
| 4 | +import { createRoot, type Root } from "react-dom/client"; |
| 5 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
| 6 | +import { usePlayerStore, type TimelineElement } from "../../player/store/playerStore"; |
| 7 | +import { useAssetPreviewStore } from "../../utils/assetPreviewStore"; |
| 8 | +import { AssetCard } from "./AssetCard"; |
| 9 | +import { AudioRow } from "./AudioRow"; |
| 10 | + |
| 11 | +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; |
| 12 | + |
| 13 | +let root: Root | null = null; |
| 14 | + |
| 15 | +afterEach(() => { |
| 16 | + if (root) { |
| 17 | + act(() => root?.unmount()); |
| 18 | + root = null; |
| 19 | + } |
| 20 | + document.body.innerHTML = ""; |
| 21 | + usePlayerStore.getState().reset(); |
| 22 | + useAssetPreviewStore.getState().clearPreviewAsset(); |
| 23 | + vi.restoreAllMocks(); |
| 24 | +}); |
| 25 | + |
| 26 | +function mount(node: React.ReactElement): HTMLElement { |
| 27 | + const host = document.createElement("div"); |
| 28 | + document.body.append(host); |
| 29 | + root = createRoot(host); |
| 30 | + act(() => { |
| 31 | + root?.render(node); |
| 32 | + }); |
| 33 | + return host; |
| 34 | +} |
| 35 | + |
| 36 | +function clip(input: Partial<TimelineElement> & { id: string; src: string }): TimelineElement { |
| 37 | + return { tag: "div", start: 0, duration: 5, track: 0, ...input }; |
| 38 | +} |
| 39 | + |
| 40 | +/** Simulate a drag-free click: pointerdown + pointerup at the same point. */ |
| 41 | +function clickCard(host: HTMLElement): void { |
| 42 | + const card = host.querySelector('[draggable="true"]'); |
| 43 | + if (!card) throw new Error("Expected a draggable card root"); |
| 44 | + const PointerCtor = (window as { PointerEvent?: typeof MouseEvent }).PointerEvent ?? MouseEvent; |
| 45 | + act(() => { |
| 46 | + card.dispatchEvent(new PointerCtor("pointerdown", { bubbles: true, clientX: 5, clientY: 5 })); |
| 47 | + card.dispatchEvent(new PointerCtor("pointerup", { bubbles: true, clientX: 5, clientY: 5 })); |
| 48 | + }); |
| 49 | +} |
| 50 | + |
| 51 | +describe("AssetCard click behavior", () => { |
| 52 | + const cardProps = { |
| 53 | + projectId: "p1", |
| 54 | + onCopy: vi.fn(), |
| 55 | + isCopied: false, |
| 56 | + }; |
| 57 | + |
| 58 | + it("clears an open preview overlay when clicking an already-added asset (reveal branch)", () => { |
| 59 | + usePlayerStore.getState().setElements([clip({ id: "img1", src: "assets/logo.png" })]); |
| 60 | + // Preview overlay is open on ANOTHER asset — the reveal must dismiss it, |
| 61 | + // or it stays stuck over the canvas while the timeline reveals the clip. |
| 62 | + useAssetPreviewStore.getState().setPreviewAsset("assets/other.png", "p1"); |
| 63 | + |
| 64 | + const host = mount(<AssetCard {...cardProps} asset="assets/logo.png" used />); |
| 65 | + clickCard(host); |
| 66 | + |
| 67 | + expect(useAssetPreviewStore.getState().previewAsset).toBeNull(); |
| 68 | + expect(usePlayerStore.getState().selectedElementId).toBe("img1"); |
| 69 | + }); |
| 70 | + |
| 71 | + it("opens the preview overlay for a not-yet-added asset", () => { |
| 72 | + const host = mount(<AssetCard {...cardProps} asset="assets/logo.png" used={false} />); |
| 73 | + clickCard(host); |
| 74 | + |
| 75 | + expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/logo.png"); |
| 76 | + expect(useAssetPreviewStore.getState().previewProjectId).toBe("p1"); |
| 77 | + }); |
| 78 | +}); |
| 79 | + |
| 80 | +describe("AudioRow click behavior", () => { |
| 81 | + const rowProps = { |
| 82 | + projectId: "p1", |
| 83 | + onCopy: vi.fn(), |
| 84 | + isCopied: false, |
| 85 | + }; |
| 86 | + |
| 87 | + it("clears an open preview overlay when clicking an already-added audio asset (reveal branch)", () => { |
| 88 | + usePlayerStore |
| 89 | + .getState() |
| 90 | + .setElements([clip({ id: "bgm1", tag: "audio", src: "assets/bgm.mp3" })]); |
| 91 | + useAssetPreviewStore.getState().setPreviewAsset("assets/other.mp3", "p1"); |
| 92 | + |
| 93 | + const host = mount(<AudioRow {...rowProps} asset="assets/bgm.mp3" used />); |
| 94 | + clickCard(host); |
| 95 | + |
| 96 | + expect(useAssetPreviewStore.getState().previewAsset).toBeNull(); |
| 97 | + expect(usePlayerStore.getState().selectedElementId).toBe("bgm1"); |
| 98 | + }); |
| 99 | + |
| 100 | + it("opens the preview overlay for a not-yet-added audio asset", () => { |
| 101 | + const host = mount(<AudioRow {...rowProps} asset="assets/bgm.mp3" used={false} />); |
| 102 | + clickCard(host); |
| 103 | + |
| 104 | + expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/bgm.mp3"); |
| 105 | + }); |
| 106 | +}); |
0 commit comments