ギャラリー内での `Shift+Enter` が引き起こす不正なルートノードを修正
ギャラリー内で Shift+Enter を押すと LineBreakNode がルートノードの直接の子として昇格し、error #99 が発生していた問題を修正しました。あわせて、コンソールエラーを検知するテストヘルパーを追加し、リグレッションテストで活用しています。
背景
Lexical のドキュメントツリーでは、ルートノードの直接の子として配置できるノードの種類に制約があります。ギャラリー内で Shift+Enter を押した際、$makeSafeForRoot が LineBreakNode をそのままルートに昇格させてしまい、不正なツリー構造として error #99 が発生していました。
この問題の根本は、$makeSafeForRoot の条件分岐が「テキストノードかどうか」という判定を起点にしていた点にあります。LineBreakNode はテキストノードではないため、ラップ処理がスキップされルートに直接配置されていました。
技術的な変更
$makeSafeForRoot のロジックを、「ルートに配置できないノードをラップする」という否定形から「ルートに配置できるノードをそのまま返す」という肯定形に反転させ、判定ロジックを $isSafeForRoot として独立させました。
変更前:
export function $makeSafeForRoot(node) {
if ($isTextNode(node)) {
return $wrapNodeInElement(node, $createParagraphNode)
} else if (node.isParentRequired()) {
const parent = node.createRequiredParent()
return $wrapNodeInElement(node, parent)
} else {
return node
}
}
変更後:
export function $isSafeForRoot(node) {
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isParentRequired()
}
export function $makeSafeForRoot(node) {
if ($isSafeForRoot(node)) {
return node
} else {
return $wrapNodeInElement(node, () => node.createParentElementNode())
}
}
旧実装では $isTextNode と isParentRequired() の2つの条件を個別に扱っていましたが、新実装では「ElementNode または DecoratorNode であり、かつ親が必須でない」という単一の肯定条件で安全性を定義しています。これにより、LineBreakNode のような条件を満たさないノードはすべてラップ対象となります。
テスト側では、コンソールエラーを収集する startMonitoringConsole 関数と、Playwright のカスタムマッチャー toHaveNoErrors が test/browser/helpers/assertions.js に追加されました。WeakMap でページインスタンスをキーにエラーを管理することで、複数ページを並走させるテスト環境でも安全に利用できます。
const consoleErrors = new WeakMap()
export function startMonitoringConsole(page) {
const errors = []
consoleErrors.set(page, errors)
page.on("pageerror", (error) => errors.push(error.message))
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text())
})
}
expect.extend({
toHaveNoErrors(page) {
// ...
},
})
リグレッションテストでは startMonitoringConsole を呼び出したうえでギャラリーを作成し、Shift+Enter を送信後にコンソールエラーがないことを検証します。Firefox では画像破損に関係する無関係なコンソールエラーが混入するため、アサーションを browserName !== "firefox" で条件分岐させています。
設計判断
$isSafeForRoot を独立した関数として切り出した点が注目されます。ルート配置可否の判定ロジックを単体でテスト・再利用できるようにすることで、将来的に他の箇所からも参照しやすくなっています。
ラップ時の処理も node.createParentElementNode() の呼び出しに統一されており、旧実装で存在した $createParagraphNode を直接渡すケースとの分岐が解消されています。これにより、各ノード型が自身の適切な親要素を決定する責務を持つという設計が一貫して適用されています。
コンソール監視ヘルパーを expect.extend によるカスタムマッチャーとして実装した点も合理的です。startMonitoringConsole(page) を呼ばずに toHaveNoErrors() を使うと明示的なエラーメッセージが返るため、テストのセットアップ漏れも検出できます。
まとめ
本PRは、ルート配置の安全性判定を「特定の不安全なケースの列挙」から「安全なノード型の明示的定義」へと反転させることでバグを修正しました。新たに導入されたコンソール監視ヘルパーは、今後のテストでも再利用可能な汎用インフラとして機能します。