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:
co-authored by
Claude Opus 5
parent
1f2cba79e9
commit
9950cb9d6b
@@ -200,9 +200,22 @@ export function AssetUploadDialog(props: Readonly<{
|
||||
// failure: "which file is this text describing" has one honest
|
||||
// answer per selection, and `submit()`'s ALT_REQUIRED refusal
|
||||
// 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" });
|
||||
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>
|
||||
|
||||
@@ -1908,3 +1908,74 @@ test("the retry after a transport failure ships its own alt text, never the firs
|
||||
{ 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",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user