From 6f7d9a856617946fc725abfb2e09baa9ceb4d7a8 Mon Sep 17 00:00:00 2001 From: dazzatronus Date: Fri, 25 Sep 2026 09:28:15 +1000 Subject: [PATCH] fix: restore timeline thumbnails after generation succeeds --- .../components/clip/clip-component.ts | 8 +-- tests/timeline-preview-errors.test.ts | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/components/timeline/components/clip/clip-component.ts b/src/components/timeline/components/clip/clip-component.ts index 9ed86635..28145bbc 100644 --- a/src/components/timeline/components/clip/clip-component.ts +++ b/src/components/timeline/components/clip/clip-component.ts @@ -239,12 +239,8 @@ export class ClipComponent { if (generating) this.element.setAttribute("aria-busy", "true"); else this.element.removeAttribute("aria-busy"); - if (state?.status === "failed") { - this.element.classList.add("ss-clip--error"); - this.element.title = state.error ?? "Generation failed"; - } else if (this.element.title && !this.currentError) { - this.element.title = ""; - } + this.element.classList.toggle("ss-clip--error", state?.status === "failed" || this.currentError !== null); + this.element.title = state?.status === "failed" ? (state.error ?? "Generation failed") : ""; } /** Show/hide error state based on clip error */ diff --git a/tests/timeline-preview-errors.test.ts b/tests/timeline-preview-errors.test.ts index 9e210db0..9fa1bd41 100644 --- a/tests/timeline-preview-errors.test.ts +++ b/tests/timeline-preview-errors.test.ts @@ -51,3 +51,64 @@ it("updates a paused timeline on asset failure and recovery, with accessible hel timeline.dispose(); container.remove(); }); + +it.each([false, true])("clears generation failure styling on retry and success (media error: %s)", async mediaError => { + const tracker = new AssetLoadTracker(); + const events = new EventEmitter(); + let generation: { status: "generating" | "failed"; error?: string } | undefined; + let loadError: { error: string; assetType: string } | null = null; + const config = { + timeline: { tracks: [{ clips: [{ id: "clip-1", asset: { type: "image", prompt: "A forest", src: "" }, start: 0, length: 5 }] }] } + }; + const edit = { + events, + assetLoader: { loadTracker: tracker }, + getInternalEvents: () => events, + getResolvedEdit: () => config, + getEdit: () => config, + isClipSelected: () => false, + getClipGenerationState: () => generation, + getClipError: () => loadError, + playbackTime: sec(0), + totalDuration: sec(5), + isPlaying: false + }; + const container = document.createElement("div"); + document.body.appendChild(container); + const timeline = new Timeline(edit as never, container); + timeline.registerClipRenderer("image", { + render: (clip, element) => { + if ("src" in clip.asset && clip.asset.src) { + element.classList.add("ss-clip--thumbnails"); + element.style.setProperty("background-image", `url("${clip.asset.src}")`); + } + } + }); + try { + await timeline.load(); + const element = container.querySelector(".ss-clip")!; + generation = { status: "failed", error: "Generation failed." }; + events.emit("clip:generationFailed", { clipId: "clip-1", error: generation.error }); + expect(element.classList.contains("ss-clip--error")).toBe(true); + expect(element.title).toBe("Generation failed."); + + loadError = mediaError ? { error: "Image failed to load.", assetType: "image" } : null; + generation = { status: "generating" }; + events.emit("clip:generationStarted", { clipId: "clip-1" }); + expect(element.classList.contains("ss-clip--error")).toBe(mediaError); + expect(element.getAttribute("aria-busy")).toBe("true"); + + config.timeline.tracks[0].clips[0].asset.src = "https://example.com/generated.png"; + generation = undefined; + events.emit("clip:generationCompleted", { clipId: "clip-1" }); + expect(element.classList.contains("ss-clip--error")).toBe(mediaError); + expect(element.hasAttribute("aria-busy")).toBe(false); + expect(element.classList.contains("ss-clip--thumbnails")).toBe(true); + expect(element.style.backgroundImage).toContain("generated.png"); + expect(element.title).toBe(""); + if (mediaError) expect(element.querySelector(".ss-clip-error-badge")).not.toBeNull(); + } finally { + timeline.dispose(); + container.remove(); + } +});