⏱️

SeleniumのWebElementの出現をauto-waitingする

に公開

はじめに

やりたいことは、Playwrightのように、ロケータで指定した要素が出現するまで待機する(auto-waiting)ことです。
SeleniumのWebElementは、要素が見つからない場合に例外を投げるため、通常のコードでは自動的に待機しません。
そのため、要素が出現するまで待機するためのラッパーをPage Object Modelの設計パターンに組み込んでで実装します。

環境

  • Python: 3.13
  • Selenium: 4.32.0
  • Appium: 2.18.0

実現方式

Explicit waitsに記述されたWebDriverWaitを使用して、要素が出現するまで待機します。
WebDriverWaitは、指定した条件が満たされるまで待機するためのクラスです。条件には、要素が存在することや、要素がクリック可能であることなどがあります。
ここでは、要素が出現するまで(visibleになるまで)待機するために、expected_conditionsモジュールのvisibility_of_element_locatedを使用します。

実装

基底クラス

以下に示すクラスを継承して、Page Object Modelのクラスを実装します。
Model.find_elementメソッドは、指定したロケータで要素を検索し、要素が出現するまで待機します。

base.py
from selenium.webdriver.support.expected_conditions import visibility_of_element_located,
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait


class Model:
    def __init__(self, driver: WebDriver, timeout: float = 10.0) -> None:
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    def find_element(self, locator: tuple[str, str]) -> WebElement:
        return self.wait.until(visibility_of_element_located(locator))

    def __getattr__(self, name: str) -> WebElement:
        if hasattr(self.__class__, name.upper()) and isinstance(getattr(self.__class__, name.upper()), tuple[str, str]):
            locator = getattr(self.__class__, name.upper())
            return self.find_element(locator)
        raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")

継承クラス

Modelクラスでは、object.__getattr__をオーバーライドしています。
Model.__getattr__メソッドは、「呼び出されたクラス属性名をすべて大文字にした変数をModel.finde_elementメソッドに渡し、その返り値をクラス属性として返す」と定義しています。言い換えれば、ロケータをクラス変数としてあらかじめ定義し、それらのロケータが指す要素(WebElement)をクラス属性として取得できるようにしています。
例えば、以下のLoginPageクラスのUSERNAMEPASSWORD, LOGIN_BUTTONのクラス変数は、ロケータ(tuple[str,str])を表し、これらのロケータに該当するWebElementusernamepasswordlogin_buttonのクラス属性として呼び出すことができます。以下の実装ではLoginPage.loginメソッドで、これらの属性を使用してログイン処理を行っています。

login_page.py
from selenium.webdriver.common.by import By
from .base import Model
class LoginPage(Model):
    USERNAME = (By.ID, "username")
    PASSWORD = (By.ID, "password")
    LOGIN_BUTTON = (By.ID, "login-button")

    def login(self, username: str, password: str) -> None:
        self.username.send_keys(username)
        self.password.send_keys(password)
        self.login_button.click()

おわりに

SeleniumにはImplicit waitsもありますが、これは要素の出現以外も含めたすべての待機に適用されるため、意図しない待機が発生してしまう可能性があると思います。ここでの実装のように明示的に待機を指定することで、必要な待機のみを行うことができます。

GitHubで編集を提案

Discussion