mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
Review 1 (restore commit): - asset reveal now clears any open preview overlay (stuck-overlay repro: preview on A, click already-added B — A stayed open over the reveal) - duration readout rolls back on failed persist: captureDurationRollback snapshots store + live root data-duration before the optimistic sync and restores both in every move/resize/delete/group catch (golden's previousDuration pattern) - asset preview opened during running playback dismisses immediately (the RAF loop bypasses the store, so the subscription alone never fired) - persistTimelineBatchEdit resolves the target (findTagByTarget) before treating identical output as a no-op — a mistargeted member now throws like the single-element path instead of being silently dropped - a post-mutation history-fold failure no longer suppresses the preview sync: fold errors are surfaced separately and the rewritten script still syncs (previously the preview kept stale GSAP positions with no recovery) - timelineRevealScroll guards degenerate viewports (windowSize <= 0) - CodeQL: encodeURIComponent(projectId) at all timelineTimingSync fetches Review 2 (single-source-of-truth pass): - createTimelineElementFromManifestClip — the one manifest->element boundary — now carries authoredTrack and stackingContextId; expanded sub-comp children preserve both (authoredTrack in their OWN file's space) - authoredTrackForLane scopes occupants to the dragged clip's sourceFile (a foreign file's authored values are a different coordinate space); nearest-same-file-lane offset fallback - optimistic store updates mirror the persisted track into authoredTrack (and roll it back on failure), so consecutive drags before a reload resolve from fresh data - spill sub-lanes: documented decision — dropping onto a spill lane is a legitimate same-track join (occupants share the authored track by construction); false 'never a lane-move target' docstring rewritten - single-element fallback persists vertical-only moves (early return now requires neither start nor track changed; live DOM patch includes data-track-index) - canonical contextKey helper for stacking-context normalization - new pipeline test crosses the REAL factory boundary (sparse authored tracks -> factory -> expansion -> normalize -> drag commit -> persisted attribute), no injected fields
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import React, { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { usePlayerStore, type TimelineElement } from "../../player/store/playerStore";
|
|
import { useAssetPreviewStore } from "../../utils/assetPreviewStore";
|
|
import { AssetCard } from "./AssetCard";
|
|
import { AudioRow } from "./AudioRow";
|
|
|
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
let root: Root | null = null;
|
|
|
|
afterEach(() => {
|
|
if (root) {
|
|
act(() => root?.unmount());
|
|
root = null;
|
|
}
|
|
document.body.innerHTML = "";
|
|
usePlayerStore.getState().reset();
|
|
useAssetPreviewStore.getState().clearPreviewAsset();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function mount(node: React.ReactElement): HTMLElement {
|
|
const host = document.createElement("div");
|
|
document.body.append(host);
|
|
root = createRoot(host);
|
|
act(() => {
|
|
root?.render(node);
|
|
});
|
|
return host;
|
|
}
|
|
|
|
function clip(input: Partial<TimelineElement> & { id: string; src: string }): TimelineElement {
|
|
return { tag: "div", start: 0, duration: 5, track: 0, ...input };
|
|
}
|
|
|
|
/** Simulate a drag-free click: pointerdown + pointerup at the same point. */
|
|
function clickCard(host: HTMLElement): void {
|
|
const card = host.querySelector('[draggable="true"]');
|
|
if (!card) throw new Error("Expected a draggable card root");
|
|
const PointerCtor = (window as { PointerEvent?: typeof MouseEvent }).PointerEvent ?? MouseEvent;
|
|
act(() => {
|
|
card.dispatchEvent(new PointerCtor("pointerdown", { bubbles: true, clientX: 5, clientY: 5 }));
|
|
card.dispatchEvent(new PointerCtor("pointerup", { bubbles: true, clientX: 5, clientY: 5 }));
|
|
});
|
|
}
|
|
|
|
describe("AssetCard click behavior", () => {
|
|
const cardProps = {
|
|
projectId: "p1",
|
|
onCopy: vi.fn(),
|
|
isCopied: false,
|
|
};
|
|
|
|
it("clears an open preview overlay when clicking an already-added asset (reveal branch)", () => {
|
|
usePlayerStore.getState().setElements([clip({ id: "img1", src: "assets/logo.png" })]);
|
|
// Preview overlay is open on ANOTHER asset — the reveal must dismiss it,
|
|
// or it stays stuck over the canvas while the timeline reveals the clip.
|
|
useAssetPreviewStore.getState().setPreviewAsset("assets/other.png", "p1");
|
|
|
|
const host = mount(<AssetCard {...cardProps} asset="assets/logo.png" used />);
|
|
clickCard(host);
|
|
|
|
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
expect(usePlayerStore.getState().selectedElementId).toBe("img1");
|
|
});
|
|
|
|
it("opens the preview overlay for a not-yet-added asset", () => {
|
|
const host = mount(<AssetCard {...cardProps} asset="assets/logo.png" used={false} />);
|
|
clickCard(host);
|
|
|
|
expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/logo.png");
|
|
expect(useAssetPreviewStore.getState().previewProjectId).toBe("p1");
|
|
});
|
|
});
|
|
|
|
describe("AudioRow click behavior", () => {
|
|
const rowProps = {
|
|
projectId: "p1",
|
|
onCopy: vi.fn(),
|
|
isCopied: false,
|
|
};
|
|
|
|
it("clears an open preview overlay when clicking an already-added audio asset (reveal branch)", () => {
|
|
usePlayerStore
|
|
.getState()
|
|
.setElements([clip({ id: "bgm1", tag: "audio", src: "assets/bgm.mp3" })]);
|
|
useAssetPreviewStore.getState().setPreviewAsset("assets/other.mp3", "p1");
|
|
|
|
const host = mount(<AudioRow {...rowProps} asset="assets/bgm.mp3" used />);
|
|
clickCard(host);
|
|
|
|
expect(useAssetPreviewStore.getState().previewAsset).toBeNull();
|
|
expect(usePlayerStore.getState().selectedElementId).toBe("bgm1");
|
|
});
|
|
|
|
it("opens the preview overlay for a not-yet-added audio asset", () => {
|
|
const host = mount(<AudioRow {...rowProps} asset="assets/bgm.mp3" used={false} />);
|
|
clickCard(host);
|
|
|
|
expect(useAssetPreviewStore.getState().previewAsset).toBe("assets/bgm.mp3");
|
|
});
|
|
});
|