クロスブラウザでテキストフィールドの内容をクリアするまでの道のり

初めまして、Autify でソフトウェアエンジニアをしている守屋です。 Autify ではクロスブラウザでの E2E テストを提供するため、Selenium(Webdriver)を採用しています。

Webdriver の動作は W3C によって標準化されており、同じコードでクロスブラウザテストができる…と期待したいところですが、現実には様々な場面で互換性に悩まされます。 今回はその中の 1 つ、「テキストフィールドの値をクリアする」という課題についてお伝えします。

テキストフィールドの値をクリアする

ユーザ名の変更が正しく動作するか、などのテストでは、テキストフィールドにすでに入力されている値を一度クリアする必要があります。 Webdriver では clear という API が用意されており、その動作は標準で定義されています。

12.4.2 Element Clear | WebDriver

非常に複雑ですが、ざっくり言うと「input や textarea 要素では value に""をセットする。contenteditable 要素では innterHTML に""をセットする」という内容です。

この動作でほとんどの場合問題ないのですが、クリア中にキーボードイベントが一切発火しないため、JS でのバリデーションが実装されているフォームでエラーが発生したり、React で作られたフォームで「テキストが消えたように見えるが内部のステートは変化していない」といったことが起こります。

自動テストの挙動によってテストが正しく行えない時は、自動テストの振る舞いをユーザが行う操作に近づけると、幅広いアプリケーションで想定通りのテストが行えるはずです。

ネタバレ

クロスブラウザでinput要素もしくはcontenteditable要素の値をクリアするには、以下のコードが必要です(サンプルコードにはPythonを使用します)。

[2020/1/27追記: コードを更新しました]

if is_firefox:
    self.driver.execute_script("""
      var element = arguments[0]
      element.focus();
      element.select ?
          element.select() :
          document.execCommand("selectAll", false, null)
    """, element)
else:
    self.driver.execute_script("""
      arguments[0].focus()
      document.execCommand("selectAll", false, null)
    """, element)

ActionChains(self.driver).send_keys(Keys.DELETE).perform()

以下にこのコードに行き着くまでの過程を紹介します。

ユーザのように振る舞う: Backspace を使う

まずは素直に、Backspace キーを使って文字を消してみましょう。入力されている文字数分だけ Backspace キーを押してみます。

text = element.get_attribute("value")
element.send_keys(Keys.BACKSPACE * len(text))

上記のコードでほとんどのケースに対応できていたのですが、Safari 12で テキスト入力の前に要素がクリックされていた場合、キャレットの位置が入力欄の先頭に置かれたままになる という挙動があり、削除に失敗することがわかりました。

またこの方法だと、対象要素が数千文字を含む textarea だった場合に要素のクリアに時間がかかってしまうため、別の方法を検討しました。

ユーザのように振る舞う: テキストを全て選択してから削除する

次に、文字列の長さが実行時間に影響しないよう Controlキー + Aでテキストを全選択してから削除してみます。

if is_mac or is_iOS:
    modifier = Keys.COMMAND
else:
    modifier = Keys.CONTROL
element.send_keys(modifier, "a")
element.send_keys(Keys.DELETE)

これでキレイに入力値がクリアできそう…に思えましたが、なぜか Mac の Chrome で値がクリアされません

調査したところ、Chrome の自動化ライブラリ Puppeteer のレポジトリに同様の issue が上がっていたので見てみます。

Puppeteer doesn’t emulate native shortcuts because native shortcuts depend on the active window, which is out of control for puppeteer.

OS が管理しているキーボードショートカットは Puppeteer では再現しないよ、とのことです。

ChromeDriver は Puppeteer と同じく Chrome DevTools Protocol に依存しており、ChromeDriver で動作しないのは同じ原因だと考えられます。残念ながら近いうちの対応を期待するのは難しそうです。

ユーザのように振舞うのを諦める: テキストを全て選択してから削除する(2)

早くも全ブラウザ共通でユーザのように値をクリアする方法がなくなってしまいました。Mac の Chrome だけ異なる操作をするのはテストの安定性に影響する可能性があるので避けたいところです。

そこで、完全にユーザの振る舞いを再現することを諦め、テキストを選択するところだけJSを使います。

JSでテキストを選択する方法はいくつかあるので、1つ1つ試してみます。

  1. execCommandを使う
    • Firefoxではinput要素でexecCommandが動作しない
  2. input要素のテキストを全選択する input.select を使う
    • iOS Safariではinput.selectが動作しない
  3. input要素では input.setSelectionRange を使う
    • OK!

ということで、冒頭に掲載したコードに行き着きます。

driver.execute_script("""
  var element = arugments[0]
  element.focus()
  element.setSelectionRange ?
    element.setSelectionRange(0, element.value.length) :
    document.execCommand("selectAll", false, null)
""", element)
element.send_keys(Keys.DELETE)

[2020/1/27追記]

その後、 input[type="email"], input[type="number"] では setSelectionRange が実装されていない ことがわかりました。全く意味がわからない。

またChromeではテキスト選択状態で element.send_keys(Keys.DELETE) を呼び出してもテキストが削除されず、ActionChainsを使用する必要がありました。

最終的に、ブラウザによって動作を分けざるをえないと判断し、以下のようなコードを使用しています。

if is_firefox:
    self.driver.execute_script("""
      var element = arguments[0]
      element.focus();
      element.select ?
        element.select() :
        document.execCommand("selectAll", false, null)
    """, element)
else:
    self.driver.execute_script("""
      arguments[0].focus()
      document.execCommand("selectAll", false, null)
    """, element)

ActionChains(self.driver).send_keys(Keys.DELETE).perform()
  • Firefox以外のブラウザでは execCommand で全要素のテキストが選択できる
  • Firefoxでは input.select が動作する

ということを踏まえ、これが最もシンプルに全環境でテキストをクリアできるコードのはずです。

[追記ここまで]

以上、Webdriver を使ってクロスブラウザで入力フィールドの値をクリアする方法でした。

テキスト入力周りではこの他にも

  • リッチテキストエディタにテキスト入力時、1文字目が入力されないことがある問題
  • Androidデバイスでソフトキーボードが表示されっぱなしになる問題
  • Mobile SafariでReactのフォームにテキストが入力できない問題

などがあるので、これらの対処法もゆくゆくお届けできればと思います。

Autify ではこのような様々な問題に立ち向かっていけるエンジニアを募集しています。興味のある方はぜひご応募ください

また、自動クロスブラウザテストをやりたい、けどこんな問題に関わってる時間はないという方は、ぜひ Autify を使ってみてください。クロスブラウザの困難は我々が対応します。現在デモリクエスト受付中です