ギャラリー内での `Shift+Enter` が引き起こす不正なルートノードを修正

basecamp/lexxy

ギャラリー内で Shift+Enter を押すと LineBreakNode がルートノードの直接の子として昇格し、error #99 が発生していた問題を修正しました。あわせて、コンソールエラーを検知するテストヘルパーを追加し、リグレッションテストで活用しています。

背景

Lexical のドキュメントツリーでは、ルートノードの直接の子として配置できるノードの種類に制約があります。ギャラリー内で Shift+Enter を押した際、$makeSafeForRootLineBreakNode をそのままルートに昇格させてしまい、不正なツリー構造として 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())
  }
}

旧実装では $isTextNodeisParentRequired() の2つの条件を個別に扱っていましたが、新実装では「ElementNode または DecoratorNode であり、かつ親が必須でない」という単一の肯定条件で安全性を定義しています。これにより、LineBreakNode のような条件を満たさないノードはすべてラップ対象となります。

テスト側では、コンソールエラーを収集する startMonitoringConsole 関数と、Playwright のカスタムマッチャー toHaveNoErrorstest/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は、ルート配置の安全性判定を「特定の不安全なケースの列挙」から「安全なノード型の明示的定義」へと反転させることでバグを修正しました。新たに導入されたコンソール監視ヘルパーは、今後のテストでも再利用可能な汎用インフラとして機能します。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
12007727

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されています。特に、任意項目である「設計判断」セクションが設けられており、コードの背後にある意図を深く解説していて素晴らしいです。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:ファイルパス)およびGitHubのPRリンク記法([#1071](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「ルートノード」「LineBreakNode」「カスタムマッチャー」などの専門用語が適切に使用されており、対象読者であるエンジニアに適した技術レベルで記述されています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、かつ各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、非常に読みやすく、理解しやすい構造になっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容の要点を正確に反映しています。ファイル名も正しく記載されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

PRやDiffで使われている `$makeSafeForRoot`, `$isSafeForRoot`, `LineBreakNode` などの技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「ルートに配置できないノードをラップする」というロジックから「ルートに配置できるノードをそのまま返す」というロジックへの変更点の解説は、技術的に正確で非常に分かりやすいです。テストヘルパーやFirefoxに関する条件分岐の説明もDiffの内容と一致しており、正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコード・コメントによって裏付けられています。ハルシネーション(捏造)は見られません。「設計判断」セクションの内容も、コードの構造から論理的に導かれる妥当な解説です。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号「#1071」やエラー番号「#99」などの数値・固有名詞は正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「ギャラリー内での `Shift+Enter` が引き起こす不正なルートノードを修正」は、PRのタイトル「Invalid root child」の内容をより具体的に、かつ正確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事には、PR情報に記載のないバージョンサポート状況やリリース日程などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

PR内の「Previously」という表現に対し、記事では「発生していた問題を修正しました」と過去形で記述しており、時間表現は正確です。