fix: clear the decorative flag when the upload dialog is handed a different file

The twin of the previous commit, with a sharper consequence. `decorative`
does not merely describe the previous image, it *exempts* it:
`validate-working-copy.ts`'s EVIDENCE_ALT_REQUIRED reads
`Asset.decorative`, so a flag inherited from a discarded divider lets a
meaningful diagram publish with no accessible name at all -- the check
passes rather than catching it. Stale alt text ships a wrong description;
stale `decorative` ships none.

Same terminal-state retry path: tick 장식용 for `divider.png`, have it
rejected, pick `sequence.png`, upload -- and `sequence.png` shipped as
decorative with `altText: undefined`.

Resetting it also re-enables the alt input, so the post-selection focus
call no longer has to ask whether it would land on a disabled control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 13:23:32 +09:00
co-authored by Claude Opus 5
parent 1f2cba79e9
commit 9950cb9d6b
2 changed files with 85 additions and 1 deletions
@@ -200,9 +200,22 @@ export function AssetUploadDialog(props: Readonly<{
// failure: "which file is this text describing" has one honest // failure: "which file is this text describing" has one honest
// answer per selection, and `submit()`'s ALT_REQUIRED refusal // answer per selection, and `submit()`'s ALT_REQUIRED refusal
// makes the emptied field impossible to ignore. // makes the emptied field impossible to ignore.
//
// `decorative` is reset for the same reason and a sharper one.
// It does not merely describe the previous image, it *exempts*
// it: `validate-working-copy.ts`'s EVIDENCE_ALT_REQUIRED reads
// `Asset.decorative`, so a flag inherited from a discarded
// divider lets a meaningful diagram publish with no accessible
// name at all -- the check passes rather than catching it. Alt
// text riding along produces a wrong description; this produces
// none.
setState({ kind: "IDLE" }); setState({ kind: "IDLE" });
setAltText(""); setAltText("");
if (!decorative) altRef.current?.focus(); setDecorative(false);
// Unconditional now: the alt field is enabled again by the line
// above, so there is no longer a case where focusing it would
// land on a disabled control.
altRef.current?.focus();
}} }}
/> />
</label> </label>
@@ -1908,3 +1908,74 @@ test("the retry after a transport failure ships its own alt text, never the firs
{ altText: "시퀀스 다이어그램", filename: "sequence.png" }, { altText: "시퀀스 다이어그램", filename: "sequence.png" },
]); ]);
}); });
// The same defect as the alt-text one above, in the flag that *exempts* an
// asset from needing alt text at all. Alt text surviving a file swap ships a
// wrong description; `decorative` surviving one ships a meaningful image
// marked as decorative -- `validate-working-copy.ts`'s EVIDENCE_ALT_REQUIRED
// reads `Asset.decorative` and so passes it, and the image reaches publication
// with no accessible name whatsoever. Worse than the bug it rides beside.
test("picking a different file after a rejected upload clears the decorative flag too", async () => {
const user = userEvent.setup();
const forms: Array<{ decorative?: boolean; altText?: string; filename: string }> = [];
const gateway = uploadOnlyGateway(async (form) => {
forms.push({
decorative: form.decorative,
altText: form.altText,
filename: form.file.name,
});
return (forms.length === 1
? { ...READY, managementStatus: "REJECTED" }
: READY) as never;
});
render(<AssetUploadDialog gateway={gateway} kind="IMAGE" onUploaded={() => {}} onClose={() => {}} />);
chooseFile(new File(["a"], "divider.png", { type: "image/png" }));
await user.click(screen.getByLabelText("장식용 이미지 (대체 텍스트 없음)"));
await user.click(screen.getByRole("button", { name: "업로드" }));
await waitFor(() =>
assert.equal(
screen.getByRole("status", { name: "업로드 상태" }).textContent,
"서버 검증에서 거절되어 사용할 수 없습니다.",
),
);
assert.deepEqual(forms, [
{ decorative: true, altText: undefined, filename: "divider.png" },
]);
// A meaningful diagram is chosen next. It must not inherit the divider's
// "no alt text needed" exemption.
chooseFile(new File(["b"], "sequence.png", { type: "image/png" }));
assert.equal(
(screen.getByLabelText("장식용 이미지 (대체 텍스트 없음)") as HTMLInputElement).checked,
false,
);
// The alt field is re-enabled by the same reset -- while `decorative` was
// still set it was `disabled`, so a cleared-but-disabled field would be a
// dead end.
assert.equal((screen.getByLabelText("대체 텍스트") as HTMLInputElement).disabled, false);
// And it is enforced: uploading now refuses instead of publishing an
// unnamed image.
await user.click(screen.getByRole("button", { name: "업로드" }));
assert.equal(
screen.getByRole("status", { name: "업로드 상태" }).textContent,
"대체 텍스트를 입력하거나 장식용으로 표시하세요.",
);
assert.equal(forms.length, 1);
await user.type(screen.getByLabelText("대체 텍스트"), "시퀀스 다이어그램");
await user.click(screen.getByRole("button", { name: "업로드" }));
await waitFor(() =>
assert.equal(
screen.getByRole("status", { name: "업로드 상태" }).textContent,
"업로드했습니다.",
),
);
assert.deepEqual(forms[1], {
decorative: false,
altText: "시퀀스 다이어그램",
filename: "sequence.png",
});
});