PAP-10499: guard video thumbnail fallback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-06 19:45:38 +00:00
1 parent a0de45ec41
commit 32d38571ed
2 files changed
+63 -2

No files matched your search

@@ -130,6 +130,44 @@ describe("ArtifactCard", () => {
container.remove();
});
it("reveals video previews if the browser does not report seek completion", () => {
vi.useFakeTimers();
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
try {
flushSync(() => {
root.render(
<ArtifactCard
artifact={makeArtifact({ mediaKind: "video", contentType: "video/mp4", contentPath: "/files/clip.mp4" })}
/>,
);
});
const video = container.querySelector("video") as HTMLVideoElement;
expect(video).not.toBeNull();
expect(video.dataset.frameReady).toBe("false");
flushSync(() => {
video.dispatchEvent(new Event("loadedmetadata", { bubbles: true }));
});
expect(video.currentTime).toBe(0.05);
expect(video.dataset.frameReady).toBe("false");
flushSync(() => {
vi.advanceTimersByTime(3000);
});
expect(video.dataset.frameReady).toBe("true");
} finally {
flushSync(() => root.unmount());
container.remove();
vi.useRealTimers();
}
});
it("renders a falling-back video placeholder when no content path exists", () => {
const markup = renderToStaticMarkup(
<ArtifactCard artifact={makeArtifact({ mediaKind: "video", contentPath: null })} />,
+25 -2
View File
@@ -1,4 +1,4 @@
import { type SyntheticEvent, useRef, useState } from "react";
import { type SyntheticEvent, useEffect, useRef, useState } from "react";
import { Download, ExternalLink, Paperclip, Play } from "lucide-react";
import type { CompanyArtifact } from "@/api/artifacts";
import { Link } from "@/lib/router";
@@ -54,6 +54,14 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
const [errored, setErrored] = useState(false);
const [frameReady, setFrameReady] = useState(false);
const thumbnailSeekRequested = useRef(false);
const frameReadyFallbackTimer = useRef<number | null>(null);
useEffect(() => {
return () => {
if (frameReadyFallbackTimer.current !== null) {
window.clearTimeout(frameReadyFallbackTimer.current);
}
};
}, []);
if (errored || !artifact.contentPath) {
return (
<PreviewFrame className="flex items-center justify-center bg-black/80">
@@ -64,7 +72,19 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
);
}
const markFrameReady = () => setFrameReady(true);
const markFrameReady = () => {
if (frameReadyFallbackTimer.current !== null) {
window.clearTimeout(frameReadyFallbackTimer.current);
frameReadyFallbackTimer.current = null;
}
setFrameReady(true);
};
const scheduleFrameReadyFallback = () => {
if (frameReadyFallbackTimer.current !== null) {
window.clearTimeout(frameReadyFallbackTimer.current);
}
frameReadyFallbackTimer.current = window.setTimeout(markFrameReady, 3000);
};
const loadThumbnailFrame = (event: SyntheticEvent<HTMLVideoElement>) => {
if (thumbnailSeekRequested.current) return;
thumbnailSeekRequested.current = true;
@@ -74,6 +94,9 @@ function VideoPreview({ artifact }: { artifact: CompanyArtifact }) {
try {
if (Math.abs(video.currentTime - seekTarget) > 0.001) {
video.currentTime = seekTarget;
scheduleFrameReadyFallback();
} else {
markFrameReady();
}
} catch {
markFrameReady();