diff --git a/desktop/src/features/messages/lib/composerEditorGeometry.test.mjs b/desktop/src/features/messages/lib/composerEditorGeometry.test.mjs index 613889f81d..20dd74509b 100644 --- a/desktop/src/features/messages/lib/composerEditorGeometry.test.mjs +++ b/desktop/src/features/messages/lib/composerEditorGeometry.test.mjs @@ -28,3 +28,66 @@ test("selection geometry returns null instead of throwing after view teardown", test("link geometry returns null instead of throwing after view teardown", () => { assert.equal(getEditorLinkRect(unmountedEditor(), 1, 2), null); }); + +function mountedEditorWithCaretGeometry() { + const coords = new Map([ + [1, { left: 10, right: 11, top: 20, bottom: 32 }], + [2, { left: 30, right: 31, top: 20, bottom: 32 }], + ]); + return { + view: { + domAtPos() { + return { node: {}, offset: 99 }; + }, + coordsAtPos(pos) { + return coords.get(pos); + }, + }, + }; +} + +function withThrowingRange(callback) { + const previousDocument = globalThis.document; + globalThis.document = { + createRange() { + throw new DOMException("stale DOM offset", "IndexSizeError"); + }, + }; + try { + callback(); + } finally { + globalThis.document = previousDocument; + } +} + +test("selection geometry keeps the caret fallback when Range construction fails", () => { + withThrowingRange(() => { + assert.deepEqual( + getEditorSelectionRect(mountedEditorWithCaretGeometry(), 1, 2), + { + left: 10, + top: 20, + width: 21, + height: 12, + right: 31, + bottom: 32, + }, + ); + }); +}); + +test("link geometry keeps the caret fallback when Range construction fails", () => { + withThrowingRange(() => { + assert.deepEqual( + getEditorLinkRect(mountedEditorWithCaretGeometry(), 1, 2), + { + left: 10, + top: 20, + width: 1, + height: 12, + right: 11, + bottom: 32, + }, + ); + }); +}); diff --git a/desktop/src/features/messages/lib/composerEditorGeometry.ts b/desktop/src/features/messages/lib/composerEditorGeometry.ts index 0dd15461f5..ad15f86d4b 100644 --- a/desktop/src/features/messages/lib/composerEditorGeometry.ts +++ b/desktop/src/features/messages/lib/composerEditorGeometry.ts @@ -15,18 +15,22 @@ export function getEditorSelectionRect( to: number, ): EditorRect | null { try { - const range = document.createRange(); - const start = editor.view.domAtPos(from); - const end = editor.view.domAtPos(to); - range.setStart(start.node, start.offset); - range.setEnd(end.node, end.offset); + try { + const range = document.createRange(); + const start = editor.view.domAtPos(from); + const end = editor.view.domAtPos(to); + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); - const clientRects = Array.from(range.getClientRects()).filter( - (rect) => rect.width > 0 || rect.height > 0, - ); - const rect = clientRects[0] ?? range.getBoundingClientRect(); - range.detach(); - if (rect.width > 0 || rect.height > 0) return rect; + const clientRects = Array.from(range.getClientRects()).filter( + (rect) => rect.width > 0 || rect.height > 0, + ); + const rect = clientRects[0] ?? range.getBoundingClientRect(); + range.detach(); + if (rect.width > 0 || rect.height > 0) return rect; + } catch { + // A mounted view can still have stale Range offsets; use caret geometry. + } const startCoords = editor.view.coordsAtPos(from); const endCoords = editor.view.coordsAtPos(to); @@ -55,23 +59,27 @@ export function getEditorLinkRect( to: number, ): EditorRect | null { try { - const range = document.createRange(); - const start = editor.view.domAtPos(from); - const end = editor.view.domAtPos(to); - range.setStart(start.node, start.offset); - range.setEnd(end.node, end.offset); + try { + const range = document.createRange(); + const start = editor.view.domAtPos(from); + const end = editor.view.domAtPos(to); + range.setStart(start.node, start.offset); + range.setEnd(end.node, end.offset); - const rect = range.getBoundingClientRect(); - range.detach(); - if (rect.width > 0 || rect.height > 0) { - return { - left: rect.left, - top: rect.top, - width: rect.width, - height: rect.height, - right: rect.right, - bottom: rect.bottom, - }; + const rect = range.getBoundingClientRect(); + range.detach(); + if (rect.width > 0 || rect.height > 0) { + return { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + right: rect.right, + bottom: rect.bottom, + }; + } + } catch { + // A mounted view can still have stale Range offsets; use caret geometry. } const coords = editor.view.coordsAtPos(from);