[ci][lexical] Bug Fix: Upgrade @playwright/test to ^1.60.0#8582
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
zurfyx
approved these changes
May 28, 2026
Captures the Firefox behavior where the DOM caret lands on a <col> inside a table's <colgroup> (an unmanaged DOM region), so we can validate the workaround. https://claude.ai/code/session_01R7nxwov2mQJp7t6M2u74nG
…lement When the DOM caret lands inside an empty/void element such as a <col> (or any unmanaged element with no children), $internalResolveSelectionPoint walks up to find a Lexical ancestor and resolves to its first descendant. The Lexical state was correct after the previous fix, but the DOM caret was left stranded inside the unmanaged region. Thread a `dirty` flag back from $internalResolveSelectionPoint through $internalResolveSelectionPoints into both callers (applyDOMRange and $internalCreateRangeSelection) so the resulting RangeSelection is marked dirty and the reconciler writes a valid DOM caret back at the resolved Lexical position. Updates the Tables.spec.mjs regression test to also assert that the DOM anchor is no longer left on <col>/<colgroup> after a forced selection into the colgroup. https://claude.ai/code/session_01R7nxwov2mQJp7t6M2u74nG
The previous fix marked the resolved selection dirty whenever the DOM caret landed on a void/empty HTMLElement, which is overly broad. Void elements that are themselves Lexical nodes (LineBreakNode <br>, empty decorator containers) already resolve to a before/after-leaf point in the parent that maps to a visually identical DOM caret; forcing a DOM rewrite there is unnecessary churn. Tighten the condition to also require that the DOM element has no Lexical key, so dirty fires only for genuinely unmanaged content (e.g. <col> inside an unmanaged <colgroup>). Decorator inputs are unaffected because isSelectionWithinEditor already rejects them before resolution runs. https://claude.ai/code/session_01R7nxwov2mQJp7t6M2u74nG
The previous condition only caught void/empty unmanaged elements like <col>, but the new DOMSlot use cases that wrap a slot in unmanaged scaffolding (contenteditable=false labels, badges, headers, etc.) can have child content. A click on a child div inside that scaffolding has no Lexical key, but childNodesLength > 0, so dirty wasn't being set and the DOM caret was left stranded outside the resolved Lexical position. Drop the void-only restriction and pivot on the immediate DOM node having no Lexical key (regardless of childNodesLength). Resolution must walk up in that case, so the resolved Lexical position is by definition elsewhere; mark dirty so the reconciler syncs the DOM caret to it. Exempt the editor root element explicitly: it has no __lexicalKey_* attribute (it's tracked in _keyToDOMMap directly under the 'root' key) but root-level clicks are a normal flow that don't need a forced DOM rewrite. Lexical-managed leaves (LineBreakNode, decorator containers) still have keys and remain unaffected; decorator inputs continue to be excluded earlier by isSelectionWithinEditor. https://claude.ai/code/session_01R7nxwov2mQJp7t6M2u74nG
Decorator nodes own their internal DOM and may manage their own selection state — not only via inputs (which isSelectionCapturedIn- DecoratorInput already handles earlier), but also via custom widgets, contenteditable=false UI, or anything else that wants the DOM caret left where the user/browser put it. Add $isSelectionCapturedInDecorator(dom) to the dirty-mark guard so a click on unmanaged content inside a DecoratorNode subtree no longer forces Lexical to rewrite the DOM caret to a position outside the decorator. Only genuinely unmanaged DOM that isn't inside a decorator (e.g. <col> in an unmanaged <colgroup>, or DOMSlot scaffolding on non-decorator nodes) still triggers DOM resync. https://claude.ai/code/session_01R7nxwov2mQJp7t6M2u74nG
This was referenced May 28, 2026
Merged
5 tasks
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
Upgrading playwright should avoid the occasional CI hang on playwright install microsoft/playwright#40747.
The bundled Firefox jumped from ~144 to 150.0.2 in playwright 1.60.0, which exposed a latent selection-resolution bug: when the DOM caret lands inside an unmanaged DOM region (e.g. a table's
<colgroup>/<col>),$internalResolveSelectionPointtreatedoffset === childNodesLength === 0as "cursor at end" and walked down to the last descendant of the nearest Lexical ancestor (e.g. the last cell of a table), without ever syncing the DOM caret back to a valid position.Fixed by:
moveSelectionToEndpath whenchildNodesLength === 0(a void/empty element has no "end").dirtyflag back from$internalResolveSelectionPointthrough$internalResolveSelectionPointsinto both callers (RangeSelection.applyDOMRangeand$internalCreateRangeSelection) so the resultingRangeSelectionis marked dirty whenever the DOM caret was in genuinely unmanaged content. The reconciler then writes a valid DOM caret back at the resolved Lexical position.__lexicalKey_*, isn't the editor root, and isn't inside aDecoratorNodesubtree (decorators own their own selection via$isSelectionCapturedInDecorator). Lexical-managed leaves like<br>(LineBreakNode) and empty decorator containers are unaffected.Test plan
Before
New regression test
Selection placed on a <col> element resolves into the first cell(inpackages/lexical-playground/__tests__/e2e/Tables.spec.mjs) fails: aftersetBaseAndExtentonto a<col>of a 2×2 table whose last cell already contains "last", the DOM caret stays on<col>, the resolved Lexical anchor lands in the last cell, and a subsequent typedXproduces"lastX"instead of a new"X"in the first cell.Selection.spec.mjs:1698("shift+arrowdown into a table, when the table is the only node, selects the whole table") fails in Firefox 150 for the same underlying reason.After
Regression test passes — DOM caret is rewritten out of
<col>to the first cell and a typedXlands there; "last" in the last cell is unchanged. All 3384 unit tests pass andtscis clean.