[lexical] Bug Fix: TextNode setMode, setTextContent and isSimpleText read the latest state - #9162
Merged
etrepum merged 2 commits intoSep 13, 2026
Conversation
…read the latest state
## Description
setMode() and setTextContent() skip the write when the requested value equals `this.__mode` / `this.__text`, and isSimpleText() reads `this.__mode`. None of them go through getLatest(). In a later update, the first setter call clones the node through getWritable(), so a reference held from the previous update is stale from then on. Calling `text.setTextContent('b')` then `text.setTextContent('a')` on a node whose text was 'a' compares against the stale 'a' and returns early, so the node keeps 'b'. setMode('token') followed by setMode('normal') leaves the node in token mode the same way, and isSimpleText() keeps answering true after setMode('token') while isToken() already says true.
This reads the latest version in those three places, the same way getMode(), isToken() and isSegmented() already do, and the way facebook#9080 fixed the LinkNode readers.
## Test plan
### Before
pnpm vitest run --project unit packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts
× setTextContent can restore the original text through a stale reference
× setMode can restore the original mode through a stale reference
× isSimpleText reflects the latest mode
AssertionError: expected 'b' to be 'a' // Object.is equality
AssertionError: expected 'token' to be 'normal' // Object.is equality
AssertionError: expected true to be false // Object.is equality
Tests 3 failed (3)
### After
✓ |unit| packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts (3 tests)
Tests 3 passed (3)
Unit suites for lexical, lexical-selection, lexical-link, lexical-markdown, lexical-text, lexical-rich-text and lexical-clipboard: 119 files, 2815 tests passed. eslint, prettier and tsc are clean. E2E browser tests were not run; the change is not browser-specific.
Dev-next-gen
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
September 12, 2026 19:46
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
reviewed
Sep 13, 2026
…orFromExtensions
## Description
The test added with the setMode/setTextContent/isSimpleText fix used
initializeUnitTest and awaited each editor.update(). Both are the legacy
testing methodology; newer unit tests build the editor with
buildEditorFromExtensions and run their updates with discrete: true, which
also removes the need for async tests.
This rewrites the file that way: one `using editor = buildEditorFromExtensions(...)`
per test, a shared helper that seeds the paragraph and returns the node
reference that goes stale, `{discrete: true}` on every update, and the
assertions moved out of the update callbacks into editor.read(). The
behaviour under test is unchanged.
## Test plan
### Before
Reverting the LexicalTextNode.ts fix and running the rewritten test:
pnpm vitest run --project unit packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts
FAIL packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts > TextNode stale state readers > setTextContent can restore the original text through a stale reference
FAIL packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts > TextNode stale state readers > setMode can restore the original mode through a stale reference
FAIL packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts > TextNode stale state readers > isSimpleText reflects the latest mode
AssertionError: expected 'b' to be 'a' // Object.is equality
AssertionError: expected 'token' to be 'normal' // Object.is equality
AssertionError: expected true to be false // Object.is equality
Tests 3 failed (3)
### After
With the fix back in place:
✓ |unit| packages/lexical/src/__tests__/unit/TextNodeStaleState.test.ts (3 tests) 22ms
Tests 3 passed (3)
pnpm vitest run --project unit packages/lexical
Test Files 318 passed (318)
Tests 5106 passed | 1 skipped (5107)
eslint, prettier and tsc are clean on the rewritten file. E2E browser tests
were not run; the change is test-only and not browser-specific.
etrepum
approved these changes
Sep 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
setMode() and setTextContent() skip the write when the requested value equals
this.__mode/this.__text, and isSimpleText() readsthis.__mode. None of them go through getLatest(). In a later update, the first setter call clones the node through getWritable(), so a reference held from the previous update is stale from then on. Callingtext.setTextContent('b')thentext.setTextContent('a')on a node whose text was 'a' compares against the stale 'a' and returns early, so the node keeps 'b'. setMode('token') followed by setMode('normal') leaves the node in token mode the same way, and isSimpleText() keeps answering true after setMode('token') while isToken() already says true.This reads the latest version in those three places, the same way getMode(), isToken() and isSegmented() already do, and the way #9080 fixed the LinkNode readers.
Test plan
Before
pnpm vitest run --project unit packages/lexical/src/tests/unit/TextNodeStaleState.test.ts
AssertionError: expected 'b' to be 'a' // Object.is equality
AssertionError: expected 'token' to be 'normal' // Object.is equality
AssertionError: expected true to be false // Object.is equality
Tests 3 failed (3)
After
✓ |unit| packages/lexical/src/tests/unit/TextNodeStaleState.test.ts (3 tests)
Tests 3 passed (3)
Unit suites for lexical, lexical-selection, lexical-link, lexical-markdown, lexical-text, lexical-rich-text and lexical-clipboard: 119 files, 2815 tests passed. eslint, prettier and tsc are clean. E2E browser tests were not run; the change is not browser-specific.
AI tools used