diff --git a/web/src/app/pattern-lab/HardwareModal.test.tsx b/web/src/app/pattern-lab/HardwareModal.test.tsx index 6a86a56a0..eb4db6989 100644 --- a/web/src/app/pattern-lab/HardwareModal.test.tsx +++ b/web/src/app/pattern-lab/HardwareModal.test.tsx @@ -132,6 +132,29 @@ describe("HardwareModal", () => { expectHeaderKept(); }); + // The prompt goes to a model by paste. When the browser refuses the write, + // the label used to stay as it was, and the paste carried whatever the + // clipboard held before. + it("says Copy failed on the button whose copy the browser refused", async () => { + const writeText = vi.fn<(text: string) => Promise>(() => + Promise.reject(new DOMException("Write permission denied.", "NotAllowedError")), + ); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } }); + try { + render(); + fireEvent.click(screen.getByText("Copy the conversion prompt")); + expect(await screen.findByText("Copy failed")).toBeTruthy(); + expect(screen.getByText("Copy .h")).toBeTruthy(); + + writeText.mockImplementation(() => Promise.resolve()); + fireEvent.click(screen.getByText("Copy failed")); + expect(await screen.findByText("Copied ✓")).toBeTruthy(); + expect(writeText).toHaveBeenCalledTimes(2); + } finally { + delete (navigator as { clipboard?: unknown }).clipboard; + } + }); + it("Esc over the Publish modal closes Publish alone and keeps the header", async () => { render(); pasteHeaderAndContinue(); diff --git a/web/src/app/pattern-lab/HardwareModal.tsx b/web/src/app/pattern-lab/HardwareModal.tsx index d0b9a1732..dda5ada6a 100644 --- a/web/src/app/pattern-lab/HardwareModal.tsx +++ b/web/src/app/pattern-lab/HardwareModal.tsx @@ -49,7 +49,7 @@ export default function HardwareModal({ const [name, setName] = useState(() => labPatternName(useLabStore.getState())); const [pastes, setPastes] = useState>({}); const [single, setSingle] = useState(""); - const [copiedKey, flashCopied] = useResettingState(null, 1200); + const [copied, flashCopied] = useResettingState<{ key: string; ok: boolean } | null>(null, 1200); const [stage, setStage] = useState("convert"); const [sendOpen, setSendOpen] = useState(false); @@ -110,12 +110,17 @@ export default function HardwareModal({ const copyText = async (key: string, text: string) => { try { await navigator.clipboard.writeText(text); - flashCopied(key); + flashCopied({ key, ok: true }); } catch { - /* clipboard may be blocked */ + // The browser refused the write. Saying nothing left the label as it + // was, and whatever the clipboard held before went to the model. + flashCopied({ key, ok: false }); } }; + const copyLabel = (key: string, idle: string) => + copied?.key === key ? (copied.ok ? "Copied ✓" : "Copy failed") : idle; + const downloadH = () => { const blob = new Blob([assembled], { type: "text/plain" }); const url = URL.createObjectURL(blob); @@ -217,7 +222,7 @@ export default function HardwareModal({ type="button" onClick={() => void copyText(`u${unit.index}`, unit.prompt)} > - {copiedKey === `u${unit.index}` ? "Copied ✓" : "Copy prompt"} + {copyLabel(`u${unit.index}`, "Copy prompt")}