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