From 4064b183e6747899ac7448aa697d200b1d536751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 25 Sep 2026 08:39:46 +0000 Subject: [PATCH] fix(studio): render a missing preview poster in either order and bank the open counts (#4479) * ci(studio): bank the open-journey counts after #4401 and allow a reasoned raise * fix(studio): render a missing preview poster whichever of the 404 and the live frame comes first --- .github/workflows/ci.yml | 2 +- .../src/components/nle/NLEPreview.test.ts | 94 ++++++++++++---- .../studio/src/components/nle/NLEPreview.tsx | 4 +- .../src/components/nle/PreviewPoster.tsx | 52 ++++++--- packages/studio/tests/e2e/perf-ceilings.json | 7 +- packages/studio/tests/e2e/perf-ratchet.mjs | 40 +++++-- .../studio/tests/e2e/perf-ratchet.test.mjs | 100 ++++++++++++++++++ 7 files changed, 255 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 680d5ecf91..34d76b148d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -785,7 +785,7 @@ jobs: cp -R packages/studio/tests/e2e/fixtures/studio-open \ packages/studio/data/projects/studio-open - name: Read the base branch's work-count ceilings - # A ceiling may not rise or disappear against the branch this PR merges into. + # A ceiling may not disappear, or rise without a new reason, against the branch this PR merges into. if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.base_ref }} diff --git a/packages/studio/src/components/nle/NLEPreview.test.ts b/packages/studio/src/components/nle/NLEPreview.test.ts index b3e78eeb27..2d61c4d265 100644 --- a/packages/studio/src/components/nle/NLEPreview.test.ts +++ b/packages/studio/src/components/nle/NLEPreview.test.ts @@ -93,23 +93,25 @@ function renderPreview( document.body.append(host); const root = createRoot(host); const iframeRef = createRef(); - - act(() => { - root.render( - React.createElement(NLEPreview, { - projectId: "timeline-edit-playground", - iframeRef, - onIframeLoad: () => {}, - previewSlots, - onShadowIframeLoad: () => {}, - onShadowReadyChange: () => {}, - onShadowError: () => {}, - setShadowIframeNode: () => {}, - resetPreviewSlots: () => {}, - fillBox, - }), - ); - }); + const render = (directUrl?: string) => + act(() => { + root.render( + React.createElement(NLEPreview, { + projectId: "timeline-edit-playground", + directUrl, + iframeRef, + onIframeLoad: () => {}, + previewSlots, + onShadowIframeLoad: () => {}, + onShadowReadyChange: () => {}, + onShadowError: () => {}, + setShadowIframeNode: () => {}, + resetPreviewSlots: () => {}, + fillBox, + }), + ); + }); + render(); const viewport = host.querySelector('[aria-label="Composition preview"]') as HTMLDivElement; const stage = host.querySelector('[data-testid="preview-zoom-stage"]') as HTMLDivElement; @@ -124,6 +126,7 @@ function renderPreview( return { host, root, + render, viewport, stage, cleanup() { @@ -296,12 +299,67 @@ describe("NLEPreview", () => { expect(poster()?.style.zIndex).toBe("2"); act(() => livePlayerProps.onReadyToShowChange?.(false)); - expect(poster()).not.toBeNull(); + expect(poster()?.hidden).toBe(false); act(() => livePlayerProps.onReadyToShowChange?.(true)); + expect(poster()?.hidden).toBe(true); + act(() => poster()?.dispatchEvent(new Event("load"))); expect(poster()).toBeNull(); view.cleanup(); }); + describe("a missing poster", () => { + const renderUrl = + "/api/projects/timeline-edit-playground/thumbnail/index.html?t=0&output=source"; + const fetchSpy = vi.fn(() => Promise.resolve(new Response())); + beforeEach(() => { + fetchSpy.mockClear(); + vi.stubGlobal("fetch", fetchSpy); + }); + afterEach(() => vi.unstubAllGlobals()); + + const settle = ( + view: ReturnType, + steps: Array<"ready" | "missing" | "loaded">, + ) => { + for (const step of steps) { + const poster = view.stage.querySelector('[data-testid="preview-poster"]'); + act(() => + step === "ready" + ? livePlayerProps.onReadyToShowChange?.(true) + : poster?.dispatchEvent(new Event(step === "missing" ? "error" : "load")), + ); + } + }; + + it("is rendered for the next open when the live frame is ready first", () => { + const view = renderPreview(); + settle(view, ["ready", "missing"]); + expect(fetchSpy.mock.calls).toEqual([[renderUrl]]); + expect(view.stage.querySelector('[data-testid="preview-poster"]')).toBeNull(); + view.cleanup(); + }); + + it("is rendered once the live frame is ready when it is missing first", () => { + const view = renderPreview(); + settle(view, ["missing"]); + expect(fetchSpy).not.toHaveBeenCalled(); + settle(view, ["ready", "ready"]); + expect(fetchSpy.mock.calls).toEqual([[renderUrl]]); + view.cleanup(); + }); + + it("is rendered on a return from a sub-composition when the live frame is ready first", () => { + const view = renderPreview(); + settle(view, ["loaded", "ready"]); + view.render("/api/projects/timeline-edit-playground/preview/comp/compositions/intro.html"); + settle(view, ["ready"]); + view.render(); + settle(view, ["ready", "missing"]); + expect(fetchSpy.mock.calls).toEqual([[renderUrl]]); + view.cleanup(); + }); + }); + it("mounts the live player once when the composition switches", () => { playerMounts.length = 0; const Harness = ({ projectId }: { projectId: string }) => { diff --git a/packages/studio/src/components/nle/NLEPreview.tsx b/packages/studio/src/components/nle/NLEPreview.tsx index 9c2b9740d5..4c9b3c8a3e 100644 --- a/packages/studio/src/components/nle/NLEPreview.tsx +++ b/packages/studio/src/components/nle/NLEPreview.tsx @@ -551,11 +551,13 @@ export const NLEPreview = memo(function NLEPreview({ /> ), )} - {poster.showPoster && ( + {poster.mountPoster && (