fix(downloads): a row whose remote picture fails too falls back to its glyph
The remote thumbnail was the last resort with no fallback of its own, so a dead CDN link drew the browser's broken-image mark. A failed remote URL is remembered like a failed poster, and the row shows its film / music glyph.
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
// render when present and be absent when the asset has no container yet (queued/running before the worker
|
// render when present and be absent when the asset has no container yet (queued/running before the worker
|
||||||
// fills it). i18n is key-passthrough here.
|
// fills it). i18n is key-passthrough here.
|
||||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||||
import { render, screen, cleanup } from "@testing-library/react";
|
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||||
|
|
||||||
vi.mock("react-i18next", async (importOriginal) => {
|
vi.mock("react-i18next", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("react-i18next")>();
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
||||||
@@ -255,4 +255,16 @@ describe("DownloadRow · no picture", () => {
|
|||||||
);
|
);
|
||||||
expect(audio.querySelector("svg.lucide-music")).not.toBeNull();
|
expect(audio.querySelector("svg.lucide-music")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
// E28 review C5: the remote thumbnail was the last resort with no fallback of its own — when it
|
||||||
|
// failed too (a dead CDN link), the row drew the browser's broken-image mark.
|
||||||
|
it("falls back to the glyph when the remote picture fails too", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<DownloadRow job={withAsset("mp4", { thumbnail_url: "https://cdn.example/dead.jpg" })} />,
|
||||||
|
);
|
||||||
|
const img = container.querySelector("img")!;
|
||||||
|
expect(img.getAttribute("src")).toBe("https://cdn.example/dead.jpg");
|
||||||
|
fireEvent.error(img);
|
||||||
|
expect(container.querySelector("img")).toBeNull();
|
||||||
|
expect(container.querySelector("svg.lucide-film")).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,7 +94,12 @@ function Thumb({ job }: { job: DownloadJob }) {
|
|||||||
// back, not written off for the life of the recycled slot. Convergent, so it belongs in render.
|
// back, not written off for the life of the recycled slot. Convergent, so it belongs in render.
|
||||||
const [failedPrimary, setFailedPrimary] = useState<string | null>(null);
|
const [failedPrimary, setFailedPrimary] = useState<string | null>(null);
|
||||||
if (failedPrimary !== null && failedPrimary !== primary) setFailedPrimary(null);
|
if (failedPrimary !== null && failedPrimary !== primary) setFailedPrimary(null);
|
||||||
const src = failedPrimary && failedPrimary === primary ? remote : primary;
|
// The remote picture is the last resort, and it fails too (a dead CDN link): remembered the same
|
||||||
|
// way, so the row falls to the glyph below instead of the browser's broken-image mark (E28 C5).
|
||||||
|
const [failedRemote, setFailedRemote] = useState<string | null>(null);
|
||||||
|
if (failedRemote !== null && failedRemote !== remote) setFailedRemote(null);
|
||||||
|
const candidate = failedPrimary && failedPrimary === primary ? remote : primary;
|
||||||
|
const src = candidate && candidate === failedRemote ? null : candidate;
|
||||||
// A generic uploaded file (media_kind='file') has no video frame to poster — it's a document/
|
// A generic uploaded file (media_kind='file') has no video frame to poster — it's a document/
|
||||||
// archive, not playable media. Show a neutral file glyph instead of an (always-empty) thumbnail.
|
// archive, not playable media. Show a neutral file glyph instead of an (always-empty) thumbnail.
|
||||||
if (job.asset?.media_kind === "file") {
|
if (job.asset?.media_kind === "file") {
|
||||||
@@ -114,6 +119,7 @@ function Thumb({ job }: { job: DownloadJob }) {
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
onError={() => {
|
onError={() => {
|
||||||
if (src !== remote) setFailedPrimary(primary);
|
if (src !== remote) setFailedPrimary(primary);
|
||||||
|
else setFailedRemote(remote);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user