【デバッグ追体験】Selenium 4のSafari DriverでiOS Safariを動かそうとしたときのデバッグログ

こんにちは。今回は自動テスト開発においてデバッグをした（そして諦めた）際のログを残してみようと思います。
SETエンジニアがどんなことを考えて試行錯誤しているのかトレースできるようにしてみたので、一種の読み物としてお楽しみいただければと思います。

## サマリ

- Selenium 4からdesired_capabilitiesがDeprecatedになり、Optionsを使うべきとWarningが出るようになった
- Safari DriverでiOS Safariを動かそうとOptionsを変えて試行錯誤したものの、iOSでなくPCのSafariが起動してしまう
- 調べた結果バグっぽいので、解消されるまで諦めてdesired capabilitiesを使うことにした

## Environment

- Python 3.10.0
- selenium 4.1.0

## Before Selenium 4

SeleniumではSafari Driverをサポートしており、
これを利用するとMac SafariだけでなくiOS Safariも動かすことができます。

SeleniumのPythonライブラリを使って、
pytest向けのテストスクリプトとして記述をすると以下のようになります。

```python
from selenium import webdriver


class TestCase:
    def test_safari(self):
        desired_caps = {
            'browserName': 'safari',
            'platform': 'ios',
        }

        driver = webdriver.Safari(desired_capabilities=desired_caps)
        driver.get("https://google.co.jp")
        driver.quit()
```

※下記記事を参考にしました。

https://qiita.com/ryuji_yasu/items/f0cb687365919ec4d3c1

## After Selenium 4

Selenium 4から、`desired_capabilities`がdeprecatedとなり、
上記コードを実行すると以下のようなWarningが出るようになりました。

```shell
DeprecationWarning: desired_capabilities has been deprecated, please use the Options class to set it
    driver = webdriver.Safari(desired_capabilities=desired_caps)
```

バージョンアップ対応もQAの仕事なので、これをどう改善したらいいか検討してみます。

## Selenium 4 with Options

Warningには「`Options`クラスを使え」とあるので、Safariの`Options`クラスを引っ張ってきます。
Safariの`Options`クラスは以下の場所にあります。

```python
from selenium.webdriver.safari.options import Options as SafariOptions
```

これをインスタンス化し、`vars()`関数を使ってデフォルト値を見ると下記のようになっていました。

```json
{
    '_caps': { 'browserName': 'safari', 'platformName': 'mac', 'pageLoadStrategy': 'normal' },
    'mobile_options': None,
    '_arguments': [],
    '_ignore_local_proxy': False,
    '_binary_location': None,
    '_preferences': {},
    'log': < selenium.webdriver.safari.options.Log object at 0x1072e4910 >
}
```

なるほど、どうも`'_caps'`が`desired_capabilities`に相当するようですね。
また`Options`には`'_caps'`をsetするメソッドである`set_capability()`も用意されていました。

つまり、これを用いて先ほどのコードを書き換えると下記のようになるはずです。

```python
from selenium import webdriver
from selenium.webdriver.safari.options import Options as SafariOptions


class TestCase:
    def test_safari_new(self):
        options = SafariOptions()
        options.set_capability("platform", "ios")
        options.set_capability("platformName", "ios") # to overwrite mac

        driver = webdriver.Safari(options=options)
        driver.get("https://google.co.jp")
```

しかしこれを実行すると、**何故かiOSではなくMacのSafariが起動しました。**

## Debugging

なんでじゃい、と思ってGitHubのソースコードを読んでみると…

https://github.com/SeleniumHQ/selenium/blob/trunk/py/selenium/webdriver/safari/webdriver.py

```python
class WebDriver(RemoteWebDriver):
    """
    Controls the SafariDriver and allows you to drive the browser.
    """

    def __init__(self, port=0, executable_path=DEFAULT_EXECUTABLE_PATH, reuse_service=False,
                 desired_capabilities=DEFAULT_SAFARI_CAPS, quiet=False,
                 keep_alive=True, service_args=None, options: Options = None, service: Service = None):
```

`desired_capabilities`にはデフォルト値`DEFAULT_SAFARI_CAPS`が渡されていて、`options`にはNoneが渡されていることが分かりました。

となると、`desired_capabilities`が`options`より内部での処理の優先順位が高い場合、
自分で設定した`options`内の`_caps`よりも`DEFAULT_SAFARI_CAPS`が優先されてしまうことになります。
もしも`DEFAULT_SAFARI_CAPS`がMac Safariを起動するような設定になっていた場合、先ほどの挙動が引き起こされます。
 
これを検証するためには、Warningは出るものの起動はする`test_safari`を以下のように書き換えて、MacとiOSのどちらのSafariが起動するか確認すればよさそうです。

```python
from selenium import webdriver
from selenium.webdriver.safari.options import Options as SafariOptions


class TestCase:
    def test_safari_debugging(self):
        options = SafariOptions() # optionsはMac Safari を起動するように指定

        desired_caps = {
            'browserName': 'safari',
            'platform': 'ios',
        } # desired_capsはiOS Safariを起動するように指定

        driver = webdriver.Safari(options=options, desired_capabilities=desired_caps)
        driver.get("https://google.co.jp")
        driver.quit()
```

これを実行すると予想通り**iOS Safariが起動しました。**

さて、`DEFAULT_SAFARI_CAPS`の実体は…

```python
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

DEFAULT_SAFARI_CAPS = DesiredCapabilities.SAFARI.copy()
```

であって、この中身を見ると以下であることが分かります。

```json
{
    'browserName': 'safari',
    'platformName': 'mac'
}
```

ビンゴっぽい。

これらのことから、先ほどの推論通り、
`options`に適切なパラメータを設定して渡しても無視されてMac Safariが起動してしまうのは、
内部で`options`より`desired_capabilities`が優先して処理されるのにも関わらず、
`desired_capabilities`に`DEFAULT_SAFARI_CAPS`が設定されているからのようであることが分かりました。

## Reporting

とはいえ、これは観測した一部分の結果からの推論にすぎません。詳細は開発者の方に確認していただく必要があります。
例えば、お気づきの方もいたかと思いますが、`SafariOptions`の中に存在している`'mobile_options': None`の記述はやはり気になりますよね。どうも中身を見る限りAndroidに関する設定項目のようだったので今回はスルーしましたが、これを適切に設定したら上手くいくという可能性もあります。

本来はここで発生した事象と再現手順と原因の推論を書いたIssueを上げてやっとQAの仕事が終わりになります。
…が、今回はWarningを無視すれば実用上の問題はないことと、他の業務があるという世知辛い理由でそこまではやれていません。尻切れトンボですみません。

## おわりに

ということで、今回はOSSにたまたまバグっぽいものが見つかったので、デバッグ作業を追体験できるような記事を書いてみました。
僕たちSETエンジニアは、普段はこんなことをしながら、動かねー動かねー動かねー・・・と99回くらい繰り返したあとでやっと動いた、みたいなことを繰り返していると知っていただけると嬉しいです。自動化 is 大変。ではまた。