diff --git a/packages/studio/src/components/renders/useRenderQueue.ts b/packages/studio/src/components/renders/useRenderQueue.ts index d4af7c61c5..44a0775a89 100644 --- a/packages/studio/src/components/renders/useRenderQueue.ts +++ b/packages/studio/src/components/renders/useRenderQueue.ts @@ -250,12 +250,19 @@ export function useRenderQueue(projectId: string | null) { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); - } catch { + } catch (err) { + // The cause used to be discarded. Every failure — a dead server, an + // aborted request, a DNS error, a mid-render crash — surfaced as the + // same sentence, and this string is what travels into the feedback + // report too, so field reports of a render that fails *every time* + // still carried nothing to act on. Keep the CLI guidance, name the + // cause after it. + const cause = err instanceof Error ? err.message : String(err); const failedJob: RenderJob = { id: generateId(), status: "failed", progress: 0, - error: "Could not reach render server. Use `hyperframes render` from the CLI instead.", + error: `Could not reach render server: ${cause}. Use \`hyperframes render\` from the CLI instead.`, filename: "Export failed", createdAt: startTime, }; diff --git a/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx b/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx new file mode 100644 index 0000000000..cb5cb98df5 --- /dev/null +++ b/packages/studio/src/components/renders/useRenderQueueTransportError.test.tsx @@ -0,0 +1,49 @@ +// @vitest-environment happy-dom + +// The render POST's catch used to discard its error, so a dead server, a DNS +// failure and a mid-render crash all produced one sentence. That string is also +// what travels into the feedback report, so three field reports — one of them a +// render that failed EVERY time — arrived with nothing to act on. + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { mountRenderQueue } from "./renderQueueTestHarness"; + +vi.mock("./useFfmpegStatus", async (importOriginal) => ({ + ...(await importOriginal()), + useFfmpegStatus: () => ({ status: { ok: true }, checking: false, recheck: vi.fn() }), +})); +vi.mock("../../telemetry/events", () => ({ trackStudioRenderStart: vi.fn() })); + +const { useRenderQueue } = await import("./useRenderQueue"); + +let queue: ReturnType | null = null; + +beforeEach(() => { + vi.stubGlobal( + "fetch", + vi.fn(() => Promise.reject(new TypeError("Failed to fetch"))), + ); +}); + +afterEach(() => { + queue?.unmount(); + queue = null; + document.body.innerHTML = ""; + vi.unstubAllGlobals(); +}); + +describe("useRenderQueue transport failure", () => { + it("names the cause instead of collapsing every failure into one sentence", async () => { + queue = mountRenderQueue(useRenderQueue); + const { act } = await import("react"); + await act(async () => { + await queue?.api().startRender({}); + }); + + const job = queue.api().jobs.at(-1); + expect(job?.status).toBe("failed"); + expect(job?.error).toContain("Failed to fetch"); + // The CLI guidance still earns its place; it just no longer stands alone. + expect(job?.error).toContain("hyperframes render"); + }); +});