fix: clear alt text when the upload dialog is handed a different file
The file input's `onChange` reset `state` to `IDLE` and left `altText` untouched. On success that is harmless -- the dialog unmounts. But REJECTED, QUARANTINED and TRANSPORT_FAILED all leave it mounted with the file input re-enabled, and that is precisely the retry path: upload `db-schema.png` described as "DB 스키마", have it quarantined, pick `sequence.png`, upload -- and `sequence.png` shipped described as "DB 스키마", passing EVIDENCE_ALT_REQUIRED and publishing with a caption about a different image. Cleared on every file selection rather than only after a failure: alt text describes one image, and "which file is this describing" has one honest answer per selection. `submit()`'s existing ALT_REQUIRED refusal turns the emptied field into a stop rather than a silent omission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f69edb633d
commit
1f2cba79e9
@@ -188,8 +188,20 @@ export function AssetUploadDialog(props: Readonly<{
|
|||||||
setState({ kind: "SELECTION_FAILED", message: MESSAGES.SELECTION_FAILED });
|
setState({ kind: "SELECTION_FAILED", message: MESSAGES.SELECTION_FAILED });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A new file clears whatever the previous attempt reported.
|
// A new file clears whatever the previous attempt reported --
|
||||||
|
// and its description with it. Alt text describes *this image*,
|
||||||
|
// and every terminal failure state (REJECTED, QUARANTINED,
|
||||||
|
// TRANSPORT_FAILED) leaves this dialog mounted with the file
|
||||||
|
// input re-enabled, so the retry path is exactly where a stale
|
||||||
|
// description survives: upload `db-schema.png` as "DB 스키마",
|
||||||
|
// get it quarantined, pick `sequence.png`, upload -- and
|
||||||
|
// `sequence.png` ships described as "DB 스키마" and validates as
|
||||||
|
// publishable. Cleared unconditionally rather than only after a
|
||||||
|
// 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.
|
||||||
setState({ kind: "IDLE" });
|
setState({ kind: "IDLE" });
|
||||||
|
setAltText("");
|
||||||
if (!decorative) altRef.current?.focus();
|
if (!decorative) altRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1835,3 +1835,76 @@ test("mergeAssetCatalog can only grow: an empty arrival keeps everything", () =>
|
|||||||
|
|
||||||
assert.deepEqual(mergeAssetCatalog([known], []).map((asset) => asset.id), [known.id]);
|
assert.deepEqual(mergeAssetCatalog([known], []).map((asset) => asset.id), [known.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Alignment follow-up, item 3: alt text must not outlive its file ---
|
||||||
|
//
|
||||||
|
// The file input's `onChange` reset `state` to `IDLE` and left `altText`
|
||||||
|
// alone. On success that is harmless -- the dialog unmounts. But REJECTED,
|
||||||
|
// QUARANTINED and TRANSPORT_FAILED all leave it mounted with the file input
|
||||||
|
// re-enabled, which is exactly the retry path: upload `db-schema.png`
|
||||||
|
// described as "DB 스키마", get it quarantined, pick `sequence.png`, upload --
|
||||||
|
// and `sequence.png` shipped described as "DB 스키마", validating as
|
||||||
|
// publishable the whole way.
|
||||||
|
|
||||||
|
test("picking a different file after a quarantined upload clears the previous image's alt text", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const forms: Array<{ altText?: string; filename: string }> = [];
|
||||||
|
const gateway = uploadOnlyGateway(async (form) => {
|
||||||
|
forms.push({ altText: form.altText, filename: form.file.name });
|
||||||
|
return (forms.length === 1
|
||||||
|
? { ...READY, managementStatus: "QUARANTINED" }
|
||||||
|
: READY) as never;
|
||||||
|
});
|
||||||
|
render(<AssetUploadDialog gateway={gateway} kind="IMAGE" onUploaded={() => {}} onClose={() => {}} />);
|
||||||
|
|
||||||
|
chooseAndUpload(new File(["a"], "db-schema.png", { type: "image/png" }), "DB 스키마");
|
||||||
|
await waitFor(() =>
|
||||||
|
assert.equal(
|
||||||
|
screen.getByRole("status", { name: "업로드 상태" }).textContent,
|
||||||
|
"보안 검사에서 격리되어 사용할 수 없습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
chooseFile(new File(["b"], "sequence.png", { type: "image/png" }));
|
||||||
|
|
||||||
|
assert.equal((screen.getByLabelText("대체 텍스트") as HTMLInputElement).value, "");
|
||||||
|
// And the emptied field is enforced, not merely displayed: uploading now
|
||||||
|
// refuses instead of shipping an undescribed image.
|
||||||
|
await user.click(screen.getByRole("button", { name: "업로드" }));
|
||||||
|
assert.equal(
|
||||||
|
screen.getByRole("status", { name: "업로드 상태" }).textContent,
|
||||||
|
"대체 텍스트를 입력하거나 장식용으로 표시하세요.",
|
||||||
|
);
|
||||||
|
assert.equal(forms.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the retry after a transport failure ships its own alt text, never the first attempt's", async () => {
|
||||||
|
const forms: Array<{ altText?: string; filename: string }> = [];
|
||||||
|
const gateway = uploadOnlyGateway(async (form) => {
|
||||||
|
forms.push({ altText: form.altText, filename: form.file.name });
|
||||||
|
if (forms.length === 1) throw new Error("offline");
|
||||||
|
return READY as never;
|
||||||
|
});
|
||||||
|
render(<AssetUploadDialog gateway={gateway} kind="IMAGE" onUploaded={() => {}} onClose={() => {}} />);
|
||||||
|
|
||||||
|
chooseAndUpload(new File(["a"], "db-schema.png", { type: "image/png" }), "DB 스키마");
|
||||||
|
await waitFor(() =>
|
||||||
|
assert.equal(
|
||||||
|
screen.getByRole("status", { name: "업로드 상태" }).textContent,
|
||||||
|
"업로드를 전송하지 못했습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
chooseAndUpload(new File(["b"], "sequence.png", { type: "image/png" }), "시퀀스 다이어그램");
|
||||||
|
await waitFor(() =>
|
||||||
|
assert.equal(
|
||||||
|
screen.getByRole("status", { name: "업로드 상태" }).textContent,
|
||||||
|
"업로드했습니다.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(forms, [
|
||||||
|
{ altText: "DB 스키마", filename: "db-schema.png" },
|
||||||
|
{ altText: "시퀀스 다이어그램", filename: "sequence.png" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user