From e1d1d673fe0f8dc7a2cdbab8f3b188ad225c0f16 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Fri, 27 Dec 2024 17:02:08 -0800 Subject: [PATCH 1/2] Failing test for #6974 --- .../6974-delete-character-backward.spec.mjs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/lexical-playground/__tests__/regression/6974-delete-character-backward.spec.mjs diff --git a/packages/lexical-playground/__tests__/regression/6974-delete-character-backward.spec.mjs b/packages/lexical-playground/__tests__/regression/6974-delete-character-backward.spec.mjs new file mode 100644 index 00000000000..703596c8bb1 --- /dev/null +++ b/packages/lexical-playground/__tests__/regression/6974-delete-character-backward.spec.mjs @@ -0,0 +1,88 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + deleteBackward, + moveToLineBeginning, +} from '../keyboardShortcuts/index.mjs'; +import { + assertHTML, + focusEditor, + html, + initialize, + test, +} from '../utils/index.mjs'; + +test.describe('Regression tests for #6974', () => { + test.beforeEach(({isPlainText, isCollab, page}) => + initialize({isCollab, isPlainText, page}), + ); + + test(`deleteCharacter merges children from adjacent blocks even if the previous leaf is an inline decorator`, async ({ + page, + isCollab, + isPlainText, + }) => { + test.skip(isCollab || isPlainText); + await focusEditor(page); + const testEquation = '$x$'; + const testString = 'test'; + await page.keyboard.type(testEquation); + await page.keyboard.press('Enter'); + await page.keyboard.type(testString); + const beforeHtml = html` +

+ + + + + + + + + +
+

+

test

+ `; + await assertHTML(page, beforeHtml, beforeHtml, { + ignoreClasses: true, + ignoreInlineStyles: true, + }); + await moveToLineBeginning(page); + await deleteBackward(page); + const afterHtml = html` +

+ + + + + + + + + + test +

+ `; + await assertHTML(page, afterHtml, afterHtml, { + ignoreClasses: true, + ignoreInlineStyles: true, + }); + }); +}); From c02e21716afe1e2b75c82de45c7854c6124d2de7 Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 30 Dec 2024 14:02:06 -0800 Subject: [PATCH 2/2] Fix getNodes() to not over-select the beginning --- packages/lexical/src/LexicalSelection.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/lexical/src/LexicalSelection.ts b/packages/lexical/src/LexicalSelection.ts index 1de9a01ffaa..30c0c8f6a01 100644 --- a/packages/lexical/src/LexicalSelection.ts +++ b/packages/lexical/src/LexicalSelection.ts @@ -96,6 +96,14 @@ export class Point { _selection: BaseSelection | null; constructor(key: NodeKey, offset: number, type: 'text' | 'element') { + if (__DEV__) { + // This prevents a circular reference error when serialized as JSON, + // which happens on unit test failures + Object.defineProperty(this, '_selection', { + enumerable: false, + writable: true, + }); + } this._selection = null; this.key = key; this.offset = offset; @@ -473,6 +481,10 @@ export class RangeSelection implements BaseSelection { const lastPoint = isBefore ? focus : anchor; let firstNode = firstPoint.getNode(); let lastNode = lastPoint.getNode(); + const overselectedFirstNode = + $isElementNode(firstNode) && + firstPoint.offset > 0 && + firstPoint.offset >= firstNode.getChildrenSize(); const startOffset = firstPoint.offset; const endOffset = lastPoint.offset; @@ -506,6 +518,13 @@ export class RangeSelection implements BaseSelection { } } else { nodes = firstNode.getNodesBetween(lastNode); + // Prevent over-selection due to the edge case of getDescendantByIndex always returning something #6974 + if (overselectedFirstNode) { + const deleteCount = nodes.findIndex( + (node) => !node.is(firstNode) && !node.isBefore(firstNode), + ); + nodes.splice(0, deleteCount); + } } if (!isCurrentlyReadOnlyMode()) { this._cachedNodes = nodes; @@ -1129,7 +1148,7 @@ export class RangeSelection implements BaseSelection { lastPoint.offset = lastNode.getTextContentSize(); } - selectedNodes.forEach((node) => { + for (const node of selectedNodes) { if ( !$hasAncestor(firstNode, node) && !$hasAncestor(lastNode, node) && @@ -1138,7 +1157,7 @@ export class RangeSelection implements BaseSelection { ) { node.remove(); } - }); + } const fixText = (node: TextNode, del: number) => { if (node.getTextContent() === '') {