fix(studio): a media preview retries after its file or project changes (#6451)

Co-authored-by: lorenzozanee <wyz0707@proton.me>
This commit is contained in:
Miguel Ángel
2026-10-26 18:15:56 +00:00
committed by GitHub
co-authored by lorenzozanee
parent 25e5ab733b
commit 15367a9f62
2 changed files with 79 additions and 5 deletions
@@ -0,0 +1,69 @@
// @vitest-environment happy-dom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, describe, expect, it } from "vitest";
import { MediaPreview } from "./MediaPreview";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
let root: Root | null = null;
let host: HTMLDivElement | null = null;
afterEach(() => {
if (root) act(() => root?.unmount());
root = null;
host = null;
document.body.replaceChildren();
});
async function renderPreview(projectId: string, filePath: string): Promise<HTMLDivElement> {
if (!root) {
host = document.createElement("div");
document.body.append(host);
root = createRoot(host);
}
await act(async () => {
root?.render(<MediaPreview projectId={projectId} filePath={filePath} />);
});
return host as HTMLDivElement;
}
describe("MediaPreview", () => {
it("retries a repaired file after visiting another media source", async () => {
const host = await renderPreview("test", "broken.png");
const failedImage = host.querySelector("img");
expect(failedImage).not.toBeNull();
await act(async () => {
failedImage?.dispatchEvent(new Event("error"));
});
expect(host.textContent).toContain("Couldn't load this file");
await renderPreview("test", "healthy.png");
expect(host.querySelector("img")?.getAttribute("src")).toContain("healthy.png");
await renderPreview("test", "broken.png");
expect(host.querySelector("img")?.getAttribute("src")).toContain("broken.png");
});
it("resets failure state when the project changes, but preserves a current failure", async () => {
const host = await renderPreview("first", "broken.png");
const image = host.querySelector("img");
await act(async () => {
image?.dispatchEvent(new Event("error"));
});
expect(host.textContent).toContain("Couldn't load this file");
await renderPreview("second", "broken.png");
expect(host.querySelector("img")?.getAttribute("src")).toContain("/second/preview/broken.png");
const secondImage = host.querySelector("img");
await act(async () => {
secondImage?.dispatchEvent(new Event("error"));
});
expect(host.textContent).toContain("Couldn't load this file");
await renderPreview("second", "broken.png");
expect(host.querySelector("img")).toBeNull();
});
});
@@ -1,5 +1,5 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useState } from "react";
import { useEffect, useState } from "react";
import { IMAGE_EXT, VIDEO_EXT, AUDIO_EXT } from "../utils/mediaTypes";
import { encodeUrlPath } from "@hyperframes/parsers";
@@ -32,10 +32,15 @@ function MediaErrorPanel({ name, filePath }: { name: string; filePath: string })
export function MediaPreview({ projectId, filePath }: { projectId: string; filePath: string }) {
const serveUrl = buildProjectApiPath(projectId, `/preview/${encodeUrlPath(filePath)}`);
const name = filePath.split("/").pop() ?? filePath;
// Keyed by path so switching to another file clears a previous failure.
const [failedPath, setFailedPath] = useState<string | null>(null);
const failed = failedPath === filePath;
const setFailed = () => setFailedPath(filePath);
const [failure, setFailure] = useState<{ source: string; failed: boolean }>({
source: serveUrl,
failed: false,
});
useEffect(() => {
setFailure({ source: serveUrl, failed: false });
}, [serveUrl]);
const failed = failure.source === serveUrl && failure.failed;
const setFailed = () => setFailure({ source: serveUrl, failed: true });
if (failed) return <MediaErrorPanel name={name} filePath={filePath} />;