Skip to content

Studio: image replace from the media picker can fail silently, and the preview shows an image that was never staged #4341

Description

@agalin920

Summary

Replacing an image from the Studio media picker can fail silently. The dialog closes, nothing is staged, no toast, no console line — and the preview still shows the new image, because the bridge command fires regardless. It looks applied and will never save.

Where

handleLayoutElementAttrUpdate (src/apps/studio/hooks/useLayoutReorderState.ts:995-1031) returns false on five preconditions — missing codeId/layoutId/attr, no cached template source for that codeId, no parse root, no [data-layout-id] match, no element at elementIndex. handleLayoutImageSrcUpdate (:1259) adds a sixth (!newSrc) and forwards the boolean.

Both call sites discard it (src/apps/studio/StudioWrapper.tsx:2510-2549):

handleLayoutImageSrcUpdate(...);      // boolean dropped
postCommandToBridge({ action: "updateImageSrc", ... });   // fires anyway
setImageEditState(null);

The inspector "Browse" branch above it has the same shape.

Why it matters beyond the UX

cypress/e2e/studio/studio-wrapper.spec.js:614saves updated image src after selecting from MediaDam — is flaky, and this is why the failure is unreadable: it lands 30s later on a missing save bar instead of at the precondition that failed.

Every run of that spec between 2026-09-15 and 2026-09-18: 10 pass, 2 fail. The two failures are 15 hours apart on unrelated branches — fix/4287-fontsize-typography-variants and fix/cv-verifier-deterministic-verdict, the latter a workflow-only diff — with a byte-identical error. retries: 1 does not rescue it; with testIsolation: false nothing is cleared between attempts, so both attempts fail.

Two candidate causes were ruled out by direct observation, not by reasoning:

  • The save bar renders under the other data-cy. StudioWrapper.tsx:1019-1027 names the single bar StudioLayoutSaveBar only when hasPendingLayoutChanges && !hasPendingContentChanges. Probed locally with a content item dirty and a layout change staged: layoutBar=1 contentBar=0. Not the cause.
  • Nothing was selected when Done was clicked. The Done button only renders inside selectedFiles?.length > 0 ? (src/apps/media/src/app/components/Header.tsx:217), so the click succeeding proves a file was selected.

Which of the six preconditions fails in CI is still unknown — precisely because the code says nothing when it gives up.

Acceptance criteria

  1. When the image-replace update returns false, the user sees a warn notification and the bridge command is not posted, so the preview does not show an image that was never staged.
  2. When it succeeds, behaviour is unchanged.
  3. The new copy exists in all six locales under public/locales/<locale>/content.json.
  4. The media picker's Done/Replace button and its selection-count element carry data-cy hooks; the Studio spec targets them instead of text-matching cy.contains("Done") (per CLAUDE.md, data-cy is the only selector strategy).
  5. The two MediaDam tests assert the selection registered before clicking Done, so a selection that did not take fails at the selection rather than 30s later at the save bar.

Known and deliberately not in scope

studio-wrapper.spec.js asserts StudioLayoutSaveBar by that exact name in ~10 places while the product renames the bar based on combined pending state. That is a latent trap, it is not this failure, and rewriting ten assertions would bury the change that matters.

This work makes the next occurrence name its own cause. It does not by itself guarantee the flake stops.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingseverity: mediumNoticeable impact on a workflow, but a workaround exists

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions