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メソッドは、指定したロケータで要素を検索し、要素が出現するまで待機します。
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クラスのUSERNAME、PASSWORD, LOGIN_BUTTONのクラス変数は、ロケータ(tuple[str,str])を表し、これらのロケータに該当するWebElementをusername、password、login_buttonのクラス属性として呼び出すことができます。以下の実装ではLoginPage.loginメソッドで、これらの属性を使用してログイン処理を行っています。
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もありますが、これは要素の出現以外も含めたすべての待機に適用されるため、意図しない待機が発生してしまう可能性があると思います。ここでの実装のように明示的に待機を指定することで、必要な待機のみを行うことができます。
Discussion