diff --git a/apps/web/src/components/PreviewDrawOverlay.tsx b/apps/web/src/components/PreviewDrawOverlay.tsx index edffc614cc..c54215a0b6 100644 --- a/apps/web/src/components/PreviewDrawOverlay.tsx +++ b/apps/web/src/components/PreviewDrawOverlay.tsx @@ -642,7 +642,16 @@ export function PreviewDrawOverlay({ let blob: Blob | null = null; const snap = await requestSnapshot(); if (snap) blob = await compositeWithBackground(snap); - if (!blob) { + if (blob) { + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + file = new File([blob], `drawing-${ts}.png`, { type: 'image/png' }); + } else if (!note.trim() && extraFiles.length === 0) { + // The snapshot pipeline is best-effort — the srcDoc foreignObject + // rasterizer legitimately fails on real-world artifacts (issue + // #4064), and retrying replays the same failure. Only block when + // the annotation has no meaning without pixels: ink/box-only marks + // are pure bitmap. A typed note or attached images still carry the + // user's intent, so those fall through and send without the shot. setCaptureWarning({ action, message: captureViewport && !hasInk && !hasBox && !hasTarget @@ -651,9 +660,8 @@ export function PreviewDrawOverlay({ }); return; } - const ts = new Date().toISOString().replace(/[:.]/g, '-'); - file = new File([blob], `drawing-${ts}.png`, { type: 'image/png' }); } + const sentWithoutScreenshot = shouldCapture && !file; const kind = markKind(); const result = await new Promise<{ ok: boolean; message?: string }>((resolve) => { let settled = false; @@ -686,7 +694,13 @@ export function PreviewDrawOverlay({ return; } clearInk(); - setCaptureWarning(null); + // Degraded sends keep the user honest about what the agent received: + // the note went out, the pixels did not. + setCaptureWarning( + sentWithoutScreenshot + ? { action, message: t('chat.annotationSentWithoutScreenshot') } + : null, + ); setNote(''); setExtraFiles([]); setPreviewIndex(null); diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index 3f9c03d917..823485e7a9 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -1505,6 +1505,7 @@ export const ar: Dict = { 'chat.annotationSendDisabledReason': 'هناك مهمة قيد التشغيل حاليًا', 'chat.annotationPreviewMissing': 'تعذّر التقاط المعاينة. يُرجى المحاولة مرة أخرى.', 'chat.annotationPreviewMissingInk': 'تعذّر التقاط المعاينة. حاول مرة أخرى لتجنّب إرسال الحبر فقط.', + 'chat.annotationSentWithoutScreenshot': 'تعذّر التقاط المعاينة. تم إرسال التعليق التوضيحي بدون لقطة شاشة.', 'chat.annotationTimeout': 'انتهت مهلة إرسال التعليق التوضيحي. يُرجى المحاولة مرة أخرى.', 'chat.annotationFailed': 'فشل إرسال التعليق التوضيحي. يُرجى المحاولة مرة أخرى.', 'chat.annotationProjectCreateFailed': 'تعذّر إنشاء مشروع، لذا لم يتم إرسال التعليق التوضيحي.', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index a6fb9a684f..73320d3db2 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -1505,6 +1505,7 @@ export const de: Dict = { 'chat.annotationSendDisabledReason': 'Eine Aufgabe wird gerade ausgeführt', 'chat.annotationPreviewMissing': 'Vorschau konnte nicht erfasst werden. Bitte versuche es erneut.', 'chat.annotationPreviewMissingInk': 'Vorschau konnte nicht erfasst werden. Versuche es erneut, um nicht nur die Tinte zu senden.', + 'chat.annotationSentWithoutScreenshot': 'Vorschau konnte nicht erfasst werden. Die Anmerkung wurde ohne Screenshot gesendet.', 'chat.annotationTimeout': 'Zeitüberschreitung beim Senden der Annotation. Bitte versuche es erneut.', 'chat.annotationFailed': 'Senden der Anmerkung fehlgeschlagen. Bitte versuche es erneut.', 'chat.annotationProjectCreateFailed': 'Es konnte kein Projekt erstellt werden, daher wurde die Anmerkung nicht gesendet.', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index a4e228dab0..c23c4580d4 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -1505,6 +1505,7 @@ export const en: Dict = { 'chat.annotationSendDisabledReason': 'A task is currently running', 'chat.annotationPreviewMissing': 'Could not capture the preview. Please try again.', 'chat.annotationPreviewMissingInk': 'Could not capture the preview. Try again to avoid sending only ink.', + 'chat.annotationSentWithoutScreenshot': 'Could not capture the preview. The annotation was sent without a screenshot.', 'chat.annotationTimeout': 'Annotation send timed out. Please try again.', 'chat.annotationFailed': 'Annotation send failed. Please try again.', 'chat.annotationProjectCreateFailed': 'Could not create a project, so the annotation was not sent.', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index 76632994d6..795d8b5a91 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -1505,6 +1505,7 @@ export const esES: Dict = { 'chat.annotationSendDisabledReason': 'Hay una tarea en ejecución', 'chat.annotationPreviewMissing': 'No se pudo capturar la vista previa. Inténtalo de nuevo.', 'chat.annotationPreviewMissingInk': 'No se pudo capturar la vista previa. Inténtalo de nuevo para evitar enviar solo la anotación.', + 'chat.annotationSentWithoutScreenshot': 'No se pudo capturar la vista previa. La anotación se envió sin captura de pantalla.', 'chat.annotationTimeout': 'Se agotó el tiempo de envío de la anotación. Inténtalo de nuevo.', 'chat.annotationFailed': 'Error al enviar la anotación. Inténtalo de nuevo.', 'chat.annotationProjectCreateFailed': 'No se pudo crear un proyecto, por lo que la anotación no se envió.', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index 3caadf930f..32f1ae5d8e 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -1505,6 +1505,7 @@ export const fa: Dict = { 'chat.annotationSendDisabledReason': 'در حال حاضر یک وظیفه در حال اجراست', 'chat.annotationPreviewMissing': 'امکان ثبت پیش‌نمایش وجود نداشت. لطفاً دوباره تلاش کنید.', 'chat.annotationPreviewMissingInk': 'امکان ثبت پیش‌نمایش وجود نداشت. برای جلوگیری از ارسال صرفِ ترسیم، دوباره تلاش کنید.', + 'chat.annotationSentWithoutScreenshot': 'پیش‌نمایش قابل ثبت نبود. حاشیه‌نویسی بدون اسکرین‌شات ارسال شد.', 'chat.annotationTimeout': 'زمان ارسال حاشیه‌نویسی به پایان رسید. لطفاً دوباره تلاش کنید.', 'chat.annotationFailed': 'ارسال حاشیه‌نویسی ناموفق بود. لطفاً دوباره تلاش کنید.', 'chat.annotationProjectCreateFailed': 'امکان ایجاد پروژه وجود نداشت، بنابراین حاشیه‌نویسی ارسال نشد.', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index afc7dadfc6..94619e9b83 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -1505,6 +1505,7 @@ export const fr: Dict = { 'chat.annotationSendDisabledReason': 'Une tâche est en cours d\'exécution', 'chat.annotationPreviewMissing': 'Impossible de capturer l\'aperçu. Veuillez réessayer.', 'chat.annotationPreviewMissingInk': 'Impossible de capturer l\'aperçu. Réessayez pour éviter de n\'envoyer que l\'annotation.', + 'chat.annotationSentWithoutScreenshot': 'Impossible de capturer l\'aperçu. L\'annotation a été envoyée sans capture d\'écran.', 'chat.annotationTimeout': 'L\'envoi de l\'annotation a expiré. Veuillez réessayer.', 'chat.annotationFailed': 'L\'envoi de l\'annotation a échoué. Veuillez réessayer.', 'chat.annotationProjectCreateFailed': 'Impossible de créer un projet, l\'annotation n\'a donc pas été envoyée.', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 35c6236f0f..ad4cce4ac0 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -1505,6 +1505,7 @@ export const hu: Dict = { 'chat.annotationSendDisabledReason': 'Jelenleg fut egy feladat', 'chat.annotationPreviewMissing': 'Nem sikerült rögzíteni az előnézetet. Kérjük, próbálja újra.', 'chat.annotationPreviewMissingInk': 'Nem sikerült rögzíteni az előnézetet. Próbálja újra, hogy ne csak a tinta legyen elküldve.', + 'chat.annotationSentWithoutScreenshot': 'Nem sikerült rögzíteni az előnézetet. A jegyzet képernyőkép nélkül lett elküldve.', 'chat.annotationTimeout': 'Az annotáció küldése időtúllépés miatt megszakadt. Kérjük, próbálja újra.', 'chat.annotationFailed': 'Az annotáció küldése nem sikerült. Kérjük, próbálja újra.', 'chat.annotationProjectCreateFailed': 'Nem sikerült projektet létrehozni, ezért az annotáció nem lett elküldve.', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index 884b4e6665..2f116eb5a9 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -1505,6 +1505,7 @@ export const id: Dict = { 'chat.annotationSendDisabledReason': 'A task is currently running', 'chat.annotationPreviewMissing': 'Could not capture the preview. Please try again.', 'chat.annotationPreviewMissingInk': 'Could not capture the preview. Try again to avoid sending only ink.', + 'chat.annotationSentWithoutScreenshot': 'Tidak dapat menangkap pratinjau. Anotasi dikirim tanpa tangkapan layar.', 'chat.annotationTimeout': 'Annotation send timed out. Please try again.', 'chat.annotationFailed': 'Annotation send failed. Please try again.', 'chat.annotationProjectCreateFailed': 'Could not create a project, so the annotation was not sent.', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index 5ba863fa3a..c4817ff499 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -1505,6 +1505,7 @@ export const it: Dict = { 'chat.annotationSendDisabledReason': 'È in esecuzione un\'attività', 'chat.annotationPreviewMissing': 'Impossibile acquisire l\'anteprima. Riprova.', 'chat.annotationPreviewMissingInk': 'Impossibile acquisire l\'anteprima. Riprova per evitare di inviare solo l\'inchiostro.', + 'chat.annotationSentWithoutScreenshot': 'Impossibile catturare l\'anteprima. L\'annotazione è stata inviata senza screenshot.', 'chat.annotationTimeout': 'Invio dell\'annotazione scaduto. Riprova.', 'chat.annotationFailed': 'Invio dell\'annotazione non riuscito. Riprova.', 'chat.annotationProjectCreateFailed': 'Impossibile creare un progetto, quindi l\'annotazione non è stata inviata.', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index a0e5a03262..33f1dc87c2 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -1505,6 +1505,7 @@ export const ja: Dict = { 'chat.annotationSendDisabledReason': 'タスクが実行中です', 'chat.annotationPreviewMissing': 'プレビューをキャプチャできませんでした。もう一度お試しください。', 'chat.annotationPreviewMissingInk': 'プレビューをキャプチャできませんでした。インクのみの送信を避けるため、もう一度お試しください。', + 'chat.annotationSentWithoutScreenshot': 'プレビューをキャプチャできませんでした。注釈はスクリーンショットなしで送信されました。', 'chat.annotationTimeout': '注釈の送信がタイムアウトしました。もう一度お試しください。', 'chat.annotationFailed': '注釈の送信に失敗しました。もう一度お試しください。', 'chat.annotationProjectCreateFailed': 'プロジェクトを作成できなかったため、注釈は送信されませんでした。', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index 3e637a653d..8c74b81225 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -1505,6 +1505,7 @@ export const ko: Dict = { 'chat.annotationSendDisabledReason': '현재 작업이 실행 중입니다', 'chat.annotationPreviewMissing': '미리보기를 캡처할 수 없습니다. 다시 시도해 주세요.', 'chat.annotationPreviewMissingInk': '미리보기를 캡처할 수 없습니다. 잉크만 전송되지 않도록 다시 시도해 주세요.', + 'chat.annotationSentWithoutScreenshot': '미리보기를 캡처하지 못했습니다. 주석은 스크린샷 없이 전송되었습니다.', 'chat.annotationTimeout': '주석 전송 시간이 초과되었습니다. 다시 시도해 주세요.', 'chat.annotationFailed': '주석 전송에 실패했습니다. 다시 시도해 주세요.', 'chat.annotationProjectCreateFailed': '프로젝트를 생성할 수 없어 주석이 전송되지 않았습니다.', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index 1305319fcf..90e667bb11 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -1505,6 +1505,7 @@ export const pl: Dict = { 'chat.annotationSendDisabledReason': 'Zadanie jest obecnie w toku', 'chat.annotationPreviewMissing': 'Nie udało się przechwycić podglądu. Spróbuj ponownie.', 'chat.annotationPreviewMissingInk': 'Nie udało się przechwycić podglądu. Spróbuj ponownie, aby nie wysłać samego rysunku.', + 'chat.annotationSentWithoutScreenshot': 'Nie udało się przechwycić podglądu. Adnotacja została wysłana bez zrzutu ekranu.', 'chat.annotationTimeout': 'Przekroczono limit czasu wysyłania adnotacji. Spróbuj ponownie.', 'chat.annotationFailed': 'Nie udało się wysłać adnotacji. Spróbuj ponownie.', 'chat.annotationProjectCreateFailed': 'Nie udało się utworzyć projektu, więc adnotacja nie została wysłana.', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 3a2eadc6b2..ef724bcd0f 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -1505,6 +1505,7 @@ export const ptBR: Dict = { 'chat.annotationSendDisabledReason': 'Uma tarefa está em execução no momento', 'chat.annotationPreviewMissing': 'Não foi possível capturar a prévia. Tente novamente.', 'chat.annotationPreviewMissingInk': 'Não foi possível capturar a prévia. Tente novamente para evitar enviar apenas o traçado.', + 'chat.annotationSentWithoutScreenshot': 'Não foi possível capturar a pré-visualização. A anotação foi enviada sem captura de tela.', 'chat.annotationTimeout': 'O envio da anotação expirou. Tente novamente.', 'chat.annotationFailed': 'Falha ao enviar a anotação. Tente novamente.', 'chat.annotationProjectCreateFailed': 'Não foi possível criar um projeto, então a anotação não foi enviada.', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 9cddc81f7e..58002940a1 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -1505,6 +1505,7 @@ export const ru: Dict = { 'chat.annotationSendDisabledReason': 'Сейчас выполняется задача', 'chat.annotationPreviewMissing': 'Не удалось сделать предпросмотр. Попробуйте ещё раз.', 'chat.annotationPreviewMissingInk': 'Не удалось сделать предпросмотр. Попробуйте ещё раз, чтобы не отправить только рукописные пометки.', + 'chat.annotationSentWithoutScreenshot': 'Не удалось захватить предпросмотр. Аннотация отправлена без скриншота.', 'chat.annotationTimeout': 'Истекло время отправки аннотации. Попробуйте ещё раз.', 'chat.annotationFailed': 'Не удалось отправить аннотацию. Попробуйте ещё раз.', 'chat.annotationProjectCreateFailed': 'Не удалось создать проект, поэтому аннотация не была отправлена.', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index 2daecd4f88..afa58f3092 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -1505,6 +1505,7 @@ export const th: Dict = { 'chat.annotationSendDisabledReason': 'มีงานกำลังทำงานอยู่', 'chat.annotationPreviewMissing': 'ไม่สามารถจับภาพตัวอย่างได้ โปรดลองอีกครั้ง', 'chat.annotationPreviewMissingInk': 'ไม่สามารถจับภาพตัวอย่างได้ ลองอีกครั้งเพื่อหลีกเลี่ยงการส่งเฉพาะลายเส้น', + 'chat.annotationSentWithoutScreenshot': 'ไม่สามารถจับภาพตัวอย่างได้ ส่งคำอธิบายประกอบโดยไม่มีภาพหน้าจอแล้ว', 'chat.annotationTimeout': 'การส่งคำอธิบายประกอบหมดเวลา โปรดลองอีกครั้ง', 'chat.annotationFailed': 'การส่งคำอธิบายประกอบล้มเหลว โปรดลองอีกครั้ง', 'chat.annotationProjectCreateFailed': 'ไม่สามารถสร้างโปรเจกต์ได้ จึงไม่ได้ส่งคำอธิบายประกอบ', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 7d7baa45c1..fe1050bba3 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -1505,6 +1505,7 @@ export const tr: Dict = { 'chat.annotationSendDisabledReason': 'Şu anda bir görev çalışıyor', 'chat.annotationPreviewMissing': 'Önizleme yakalanamadı. Lütfen tekrar deneyin.', 'chat.annotationPreviewMissingInk': 'Önizleme yakalanamadı. Yalnızca çizim göndermemek için tekrar deneyin.', + 'chat.annotationSentWithoutScreenshot': 'Önizleme yakalanamadı. Açıklama ekran görüntüsü olmadan gönderildi.', 'chat.annotationTimeout': 'Açıklama gönderimi zaman aşımına uğradı. Lütfen tekrar deneyin.', 'chat.annotationFailed': 'Açıklama gönderimi başarısız oldu. Lütfen tekrar deneyin.', 'chat.annotationProjectCreateFailed': 'Bir proje oluşturulamadığından açıklama gönderilmedi.', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index caf913d609..8e0ef1d73c 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -1505,6 +1505,7 @@ export const uk: Dict = { 'chat.annotationSendDisabledReason': 'Наразі виконується завдання', 'chat.annotationPreviewMissing': 'Не вдалося захопити попередній перегляд. Спробуйте ще раз.', 'chat.annotationPreviewMissingInk': 'Не вдалося захопити попередній перегляд. Спробуйте ще раз, щоб не надіслати лише рукописні позначки.', + 'chat.annotationSentWithoutScreenshot': 'Не вдалося захопити попередній перегляд. Анотацію надіслано без знімка екрана.', 'chat.annotationTimeout': 'Час очікування надсилання анотації вичерпано. Спробуйте ще раз.', 'chat.annotationFailed': 'Не вдалося надіслати анотацію. Спробуйте ще раз.', 'chat.annotationProjectCreateFailed': 'Не вдалося створити проєкт, тому анотацію не надіслано.', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 92347f114b..3c76796f50 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -1505,6 +1505,7 @@ export const zhCN: Dict = { 'chat.annotationSendDisabledReason': '当前正有任务在执行', 'chat.annotationPreviewMissing': '无法截到下面的预览,请重试', 'chat.annotationPreviewMissingInk': '无法截到下面的预览,请重试,避免只附带笔迹', + 'chat.annotationSentWithoutScreenshot': '未能截取预览,已发送标注(不含截图)。', 'chat.annotationTimeout': '标注发送超时,请重试', 'chat.annotationFailed': '标注发送失败,请重试', 'chat.annotationProjectCreateFailed': '无法创建项目,标注未发送', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 3145ca8966..112d0b7567 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -1505,6 +1505,7 @@ export const zhTW: Dict = { 'chat.annotationSendDisabledReason': '目前正有任務執行中', 'chat.annotationPreviewMissing': '無法截到下方預覽,請重試', 'chat.annotationPreviewMissingInk': '無法截到下方預覽,請重試,避免只附帶筆跡', + 'chat.annotationSentWithoutScreenshot': '未能擷取預覽,已傳送標註(不含截圖)。', 'chat.annotationTimeout': '標註傳送逾時,請重試', 'chat.annotationFailed': '標註傳送失敗,請重試', 'chat.annotationProjectCreateFailed': '無法建立專案,因此未送出標註。', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index ed90f42429..8e30097d06 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -2052,6 +2052,7 @@ export interface Dict { 'chat.annotationSendDisabledReason': string; 'chat.annotationPreviewMissing': string; 'chat.annotationPreviewMissingInk': string; + 'chat.annotationSentWithoutScreenshot': string; 'chat.annotationTimeout': string; 'chat.annotationFailed': string; 'chat.annotationProjectCreateFailed': string; diff --git a/apps/web/tests/components/PreviewDrawOverlay.capture-fallback.test.tsx b/apps/web/tests/components/PreviewDrawOverlay.capture-fallback.test.tsx new file mode 100644 index 0000000000..a294321eaf --- /dev/null +++ b/apps/web/tests/components/PreviewDrawOverlay.capture-fallback.test.tsx @@ -0,0 +1,133 @@ +// @vitest-environment jsdom + +// Issue #4064: the srcDoc foreignObject snapshot bridge legitimately fails on +// real-world artifacts (Chromium often refuses to rasterize +// HTML loaded via ). A failed screenshot must not dead-end an annotation +// that carries its own meaning without pixels (typed note / attached images) — +// the retry warning is a dead end because retrying the same pipeline fails the +// same way. Ink/box-only annotations still block: without the bitmap there is +// nothing to send. + +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { PreviewDrawOverlay } from '../../src/components/PreviewDrawOverlay'; +import { requestPreviewSnapshot } from '../../src/runtime/exports'; + +vi.mock('../../src/runtime/exports', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + // The snapshot bridge fails — the shape every foreignObject failure mode + // (empty-render, snapshot image failed, timeout) collapses into. + requestPreviewSnapshot: vi.fn(async () => null), + }; +}); + +let restoreRect: (() => void) | null = null; + +beforeEach(() => { + // jsdom reports 0x0 client rects, which would collapse the drawn selection + // box into nothing. Give the ink canvas a real-looking rect so pointer + // events produce a valid normalized box, like in a browser. + const rectSpy = vi + .spyOn(HTMLCanvasElement.prototype, 'getBoundingClientRect') + .mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + width: 320, + height: 200, + right: 320, + bottom: 200, + toJSON: () => ({}), + } as DOMRect); + restoreRect = () => rectSpy.mockRestore(); +}); + +afterEach(() => { + cleanup(); + restoreRect?.(); + restoreRect = null; + vi.mocked(requestPreviewSnapshot).mockClear(); +}); + +function drawSelectionBox(canvas: HTMLCanvasElement) { + fireEvent.pointerDown(canvas, { clientX: 40, clientY: 30, pointerId: 1 }); + fireEvent.pointerMove(canvas, { clientX: 220, clientY: 150, pointerId: 1 }); + fireEvent.pointerUp(canvas, { clientX: 220, clientY: 150, pointerId: 1 }); +} + +describe('PreviewDrawOverlay capture fallback (issue #4064)', () => { + it('sends a box annotation with a typed note even when the snapshot fails', async () => { + const annotation = vi.fn((event: Event) => { + const detail = (event as CustomEvent<{ ack?: (result: { ok: boolean }) => void }>).detail; + detail.ack?.({ ok: true }); + }); + window.addEventListener('opendesign:annotation', annotation); + + try { + const { container, getByRole, getByText } = render( + +