qr-send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Seleniumでブロックエディタに自動入力する:send_keysが効かない領域はキーボードショートカットで制御する

Selenium・自動化

リッチテキストエディタは「テキストボックス」ではない

管理画面の記事投稿フォームなどでよく使われている、段落やリストをブロック単位で編集するタイプのリッチテキストエディタ(いわゆるブロックエディタ)は、Seleniumで自動操作しようとすると単純なテキストボックスとは勝手がまったく違います。最初にこれを知らずに書くと、動くはずのコードがなぜか通らず、原因調査に時間を溶かしがちです。

普通の入力欄なら

driver.find_element(:css, "input[name='title']").send_keys("こんにちは")

で済みますが、ブロックエディタの本文領域は「div要素の集合」であり、改行やリスト化はテキストとして送るのではなく、エディタ側にキーボードショートカットとして解釈させる必要があります。send_keysで改行文字\nを送っても単純に改行が入るだけでブロックが分割されなかったり、意図した挙動にならなかったりすることが多いです。つまりこの手のエディタを自動操作するコツは、「文字を入力する」ではなく「人間がキーボードで操作している状態を再現する」という発想に切り替えることにあります。

既存テキストを消すにはCtrl+A→Delete

入力欄をクリアしたいとき、clearメソッドが効かないブロック単位のリッチエディタでは、キーボードショートカットで全選択してから削除する方法が確実です。

def clear_rich_text(driver, element)
  element.click
  driver.action.key_down(:control).send_keys('a').key_up(:control).perform
  driver.action.send_keys(:delete).perform
end

driver.actionはSeleniumのActionsクラスへの入り口で、key_down/key_upを組で呼ぶことで「キーを押しっぱなしにした状態で別のキーを送る」という複合操作を表現できます。ここではCtrlを押しながらAを送って全選択し、その後Deleteで消しています。要素をクリックしてフォーカスを当ててから操作している点も地味に重要で、フォーカスが外れたままキーを送ると別の場所に入力されてしまいます。

改行はShift+Enter、確定はEnter

ブロックエディタでは、素のEnterが「新しいブロックを作る」ことを意味し、同じブロック内で行だけ変えたい場合はShift+Enterを使う、という設計になっていることがよくあります。箇条書きの各行や段落内の改行を再現したい場合は、この違いを意識してキーを送り分ける必要があります。

def send_line(driver, text, hard_break: true)
  driver.find_element(:css, ".rich-text-body").send_keys(text)
  if hard_break
    driver.action.key_down(:shift).send_keys(:enter).key_up(:shift).perform
  else
    driver.action.send_keys(:enter).perform
  end
end

同一ブロック内で改行を続けたいときはhard_break: true(Shift+Enter)、リストの次の項目に進むなどブロックを切り替えたいときはhard_break: false(Enter)、という具合に呼び分けます。この区別を怠ると、段落のつもりで送った改行が新しいブロックとして扱われ、レイアウトが崩れた状態で保存されることになります。

リスト化はショートカットキーではなくオートフォーマットに任せる

テキストの先頭に- のような記号を送るとエディタ側が自動でリストブロックに変換してくれる実装も多くあります。これを利用すると、1行ずつ「今は通常の段落なのかリストの途中なのか」を判定しながら入力できます。

def type_lines(driver, body_selector, lines)
  lines.each_with_index do |line, i|
    is_list_line = line.start_with?("- ")
    text = is_list_line ? line.sub(/^-\s/, "") : line

    driver.find_element(:css, body_selector).send_keys(text)

    next if i == lines.length - 1

    if lines[i + 1].start_with?("- ")
      driver.action.send_keys(:enter).perform # リストの次の項目へ
    else
      driver.action.key_down(:shift).send_keys(:enter).key_up(:shift).perform # 同じブロック内で改行
    end
  end
end

先頭の- をそのままsend_keysすると、多くのブロックエディタはオートフォーマット機能でその段落を自動的に箇条書きリストに変換してくれます。あとは項目間でEnter(リストを継続)を送るかShift+Enter(同一ブロック内改行)を送るかを次の行の内容で判定するだけです。ショートカットキーでブロックの種類を明示的に切り替えるより、この方が対象の実装差異に振られにくく、結果的にコードもシンプルになります。

入力後は必ずテキストの一致を待って確認する

キーボード操作は非同期にDOMへ反映されるため、送信した直後に次の操作に進むとまだ反映が終わっていないことがあります。Waitを使って、期待するテキストが実際に表示されるまで待つ一手間を必ず挟みます。

wait = Selenium::WebDriver::Wait.new(timeout: 10)
wait.until { driver.find_element(:css, ".rich-text-body").text.include?(expected_fragment) }

これを省略すると、キー入力の途中で次のフィールド操作に進んでしまい、意図しない場所に文字が混入するという厄介な不具合につながります。特にリスト変換やブロック分割はDOM構造そのものが変わる操作なので、見た目のタイミングよりも一段階余裕を持って待つくらいがちょうどいい塩梅です。

まとめ

  • ブロック単位のリッチテキストエディタは通常のテキストボックスとして扱えない。人間のキーボード操作を再現する発想でコードを書く
  • クリアはCtrl+A→Delete、同一ブロック内の改行はShift+Enter、ブロックの確定・区切りはEnterと使い分ける
  • リスト化は記号付きテキストを送ってオートフォーマットに任せるのが手軽。次の行がリスト継続かどうかで送るキーを判定する
  • キー入力は非同期に反映されるため、次の操作に進む前にWaitで反映結果を確認してから進めると事故が減る

コメント

タイトルとURLをコピーしました