Most of the documentation found in this section is still in English.
Please note we are not accepting pull requests to translate this content
as translating documentation of legacy components does not add value to
the community nor the project.
Over time, projects tend to accumulate large numbers of tests. As the total number of tests increases,
it becomes harder to make changes to the codebase — a single “simple” change may
cause numerous tests to fail, even though the application still works properly.
Sometimes these problems are unavoidable, but when they do occur you want to be
up and running again as quickly as possible. The following design patterns and
strategies have been used before with WebDriver to help to make tests easier to write and maintain. They may help you too.
DomainDrivenDesign: Express your tests in the language of the end-user of the app.
PageObjects: A simple abstraction of the UI of your web app.
LoadableComponent: Modeling PageObjects as components.
BotStyleTests: Using a command-based approach to automating tests, rather than the object-based approach that PageObjects encourage
Loadable Component
What Is It?
The LoadableComponent is a base class that aims to make writing PageObjects
less painful. It does this by providing a standard way of ensuring that pages
are loaded and providing hooks to make debugging the failure of a page to load
easier. You can use it to help reduce the amount of boilerplate code in your
tests, which in turn make maintaining your tests less tiresome.
There is currently an implementation in Java that ships as part of Selenium 2,
but the approach used is simple enough to be implemented in any language.
Simple Usage
As an example of a UI that we’d like to model, take a look at
the new issue page.
From the point of view of a test author, this offers the service of being
able to file a new issue. A basic Page Object would look like:
In order to turn this into a LoadableComponent, all we need to do is to set that as the base type:
publicclassEditIssueextendsLoadableComponent<EditIssue>{// rest of class ignored for now}
This signature looks a little unusual, but it all means is that
this class represents a LoadableComponent that loads the EditIssue page.
By extending this base class, we need to implement two new methods:
@Overrideprotectedvoidload(){driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+");}@OverrideprotectedvoidisLoaded()throwsError{Stringurl=driver.getCurrentUrl();assertTrue("Not on the issue entry page: "+url,url.endsWith("/new"));}
The load method is used to navigate to the page, whilst the isLoaded method
is used to determine whether we are on the right page. Although the
method looks like it should return a boolean, instead it performs a
series of assertions using JUnit’s Assert class. There can be as
few or as many assertions as you like. By using these assertions
it’s possible to give users of the class clear information that
can be used to debug tests.
With a little rework, our PageObject looks like:
packagecom.example.webdriver;importorg.openqa.selenium.By;importorg.openqa.selenium.WebDriver;importorg.openqa.selenium.WebElement;importorg.openqa.selenium.support.FindBy;importorg.openqa.selenium.support.PageFactory;import staticjunit.framework.Assert.assertTrue;publicclassEditIssueextendsLoadableComponent<EditIssue>{privatefinalWebDriverdriver;// By default the PageFactory will locate elements with the same name or id// as the field. Since the issue_title element has an id attribute of "issue_title"// we don't need any additional annotations.privateWebElementissue_title;// But we'd prefer a different name in our code than "issue_body", so we use the// FindBy annotation to tell the PageFactory how to locate the element.@FindBy(id="issue_body")privateWebElementbody;publicEditIssue(WebDriverdriver){this.driver=driver;// This call sets the WebElement fields.PageFactory.initElements(driver,this);}@Overrideprotectedvoidload(){driver.get("https://github.com/SeleniumHQ/selenium/issues/new?assignees=&labels=I-defect%2Cneeds-triaging&projects=&template=bug-report.yml&title=%5B%F0%9F%90%9B+Bug%5D%3A+");}@OverrideprotectedvoidisLoaded()throwsError{Stringurl=driver.getCurrentUrl();assertTrue("Not on the issue entry page: "+url,url.endsWith("/new"));}publicvoidsetHowToReproduce(StringhowToReproduce){WebElementfield=driver.findElement(By.id("issue_form_repro-command"));clearAndType(field,howToReproduce);}publicvoidsetLogOutput(StringlogOutput){WebElementfield=driver.findElement(By.id("issue_form_logs"));clearAndType(field,logOutput);}publicvoidsetOperatingSystem(StringoperatingSystem){WebElementfield=driver.findElement(By.id("issue_form_operating-system"));clearAndType(field,operatingSystem);}publicvoidsetSeleniumVersion(StringseleniumVersion){WebElementfield=driver.findElement(By.id("issue_form_selenium-version"));clearAndType(field,logOutput);}publicvoidsetBrowserVersion(StringbrowserVersion){WebElementfield=driver.findElement(By.id("issue_form_browser-versions"));clearAndType(field,browserVersion);}publicvoidsetDriverVersion(StringdriverVersion){WebElementfield=driver.findElement(By.id("issue_form_browser-driver-versions"));clearAndType(field,driverVersion);}publicvoidsetUsingGrid(StringusingGrid){WebElementfield=driver.findElement(By.id("issue_form_selenium-grid-version"));clearAndType(field,usingGrid);}publicIssueListsubmit(){driver.findElement(By.cssSelector("button[type='submit']")).click();returnnewIssueList(driver);}privatevoidclearAndType(WebElementfield,Stringtext){field.clear();field.sendKeys(text);}}
That doesn’t seem to have bought us much, right? One thing it has done
is encapsulate the information about how to navigate to the page into
the page itself, meaning that this information’s not scattered through
the code base. It also means that we can do this in our tests:
EditIssuepage=newEditIssue(driver).get();
This call will cause the driver to navigate to the page if that’s necessary.
Nested Components
LoadableComponents start to become more useful when they are used in
conjunction with other LoadableComponents. Using our example, we could
view the “edit issue” page as a component within a project’s website
(after all, we access it via a tab on that site). You also need to be
logged in to file an issue. We could model this as a tree of nested components:
+ ProjectPage
+---+ SecuredPage
+---+ EditIssue
What would this look like in code? For a start, each logical component
would have its own class. The “load” method in each of them would “get”
the parent. The end result, in addition to the EditIssue class above is:
packagecom.example.webdriver;importorg.openqa.selenium.By;importorg.openqa.selenium.NoSuchElementException;importorg.openqa.selenium.WebDriver;importorg.openqa.selenium.WebElement;import staticorg.junit.Assert.fail;publicclassSecuredPageextendsLoadableComponent<SecuredPage>{privatefinalWebDriverdriver;privatefinalLoadableComponent<?>parent;privatefinalStringusername;privatefinalStringpassword;publicSecuredPage(WebDriverdriver,LoadableComponent<?>parent,Stringusername,Stringpassword){this.driver=driver;this.parent=parent;this.username=username;this.password=password;}@Overrideprotectedvoidload(){parent.get();StringoriginalUrl=driver.getCurrentUrl();// Sign indriver.get("https://www.google.com/accounts/ServiceLogin?service=code");driver.findElement(By.name("Email")).sendKeys(username);WebElementpasswordField=driver.findElement(By.name("Passwd"));passwordField.sendKeys(password);passwordField.submit();// Now return to the original URLdriver.get(originalUrl);}@OverrideprotectedvoidisLoaded()throwsError{// If you're signed in, you have the option of picking a different login.// Let's check for the presence of that.try{WebElementdiv=driver.findElement(By.id("multilogin-dropdown"));}catch(NoSuchElementExceptione){fail("Cannot locate user name link");}}}
This shows that the components are all “nested” within each other.
A call to get() in EditIssue will cause all its dependencies to load too. The example usage:
If you’re using a library such as Guiceberry in your tests,
the preamble of setting up the PageObjects can be omitted leading to nice, clear, readable tests.
Although PageObjects are a useful way of reducing duplication in your tests,
it’s not always a pattern that teams feel comfortable following.
An alternative approach is to follow a more “command-like” style of testing.
A “bot” is an action-oriented abstraction over the raw Selenium APIs.
This means that if you find that commands aren’t doing the Right Thing
for your app, it’s easy to change them. As an example:
publicclassActionBot{privatefinalWebDriverdriver;publicActionBot(WebDriverdriver){this.driver=driver;}publicvoidclick(Bylocator){driver.findElement(locator).click();}publicvoidsubmit(Bylocator){driver.findElement(locator).submit();}/**
* Type something into an input field. WebDriver doesn't normally clear these
* before typing, so this method does that first. It also sends a return key
* to move the focus out of the element.
*/publicvoidtype(Bylocator,Stringtext){WebElementelement=driver.findElement(locator);element.clear();element.sendKeys(text+"\n");}}
Once these abstractions have been built and duplication in your tests
identified, it’s possible to layer PageObjects on top of bots.
"""
An example of `python + pytest + selenium`
which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
"""importpytestfromseleniumimportwebdriverfromselenium.commonimport(ElementNotInteractableException,NoSuchElementException,StaleElementReferenceException,)fromselenium.webdriverimportActionChainsfromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webelementimportWebElementfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait@pytest.fixture(scope="function")defchrome_driver():withwebdriver.Chrome()asdriver:driver.set_window_size(1024,768)driver.implicitly_wait(0.5)yielddriverclassActionBot:def__init__(self,driver)->None:self.driver=driverself.wait=WebDriverWait(driver,timeout=10,poll_frequency=2,ignored_exceptions=[NoSuchElementException,StaleElementReferenceException,ElementNotInteractableException,],)defelement(self,locator:tuple)->WebElement:self.wait.until(lambdadriver:driver.find_element(*locator))returnself.driver.find_element(*locator)defelements(self,locator:tuple)->list[WebElement]:returnself.driver.find_elements(*locator)defhover(self,locator:tuple)->None:element=self.element(locator)ActionChains(self.driver).move_to_element(element).perform()defclick(self,locator:tuple)->None:element=self.element(locator)element.click()deftype(self,locator:tuple,value:str)->None:element=self.element(locator)element.clear()element.send_keys(value)deftext(self,locator:tuple)->str:element=self.element(locator)returnelement.textclassLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnselfclassTodoPage(LoadableComponent):url="https://todomvc.com/examples/react/dist/"new_todo_by=(By.CSS_SELECTOR,"input.new-todo")count_todo_left_by=(By.CSS_SELECTOR,"span.todo-count")todo_items_by=(By.CSS_SELECTOR,"ul.todo-list>li")view_all_by=(By.LINK_TEXT,"All")view_active_by=(By.LINK_TEXT,"Active")view_completed_by=(By.LINK_TEXT,"Completed")toggle_all_by=(By.CSS_SELECTOR,"input.toggle-all")clear_completed_by=(By.CSS_SELECTOR,"button.clear-completed")@staticmethoddefbuild_todo_by(s:str)->tuple:p=f"//li[.//label[contains(text(), '{s}')]]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_label_by(s:str)->tuple:p=f"//label[contains(text(), '{s}')]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_toggle_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../input[@class='toggle']"returnby,p@staticmethoddefbuild_todo_item_delete_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../button[@class='destroy']"returnby,pdefbuild_count_todo_left(self,count:int)->str:ifcount==1:return"1 item left!"else:returnf"{count} items left!"def__init__(self,driver):self.driver=driverself.bot=ActionBot(driver)defload(self):self.driver.get(self.url)defis_loaded(self):try:WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.new_todo_by))returnTrueexcept:returnFalse# business domain belowdefcount_todo_items_left(self)->str:returnself.bot.text(self.count_todo_left_by)deftodo_count(self)->int:returnlen(self.bot.elements(self.todo_items_by))defnew_todo(self,s:str):self.bot.type(self.new_todo_by,s+"\n")deftoggle_todo(self,s:str):self.bot.click(self.build_todo_item_toggle_by(s))defhover_todo(self,s:str)->None:self.bot.hover(self.build_todo_by(s))defdelete_todo(self,s:str):self.hover_todo(s)self.bot.click(self.build_todo_item_delete_by(s))defclear_completed_todo(self):self.bot.click(self.clear_completed_by)deftoggle_all_todo(self):self.bot.click(self.toggle_all_by)defview_all_todo(self):self.bot.click(self.view_all_by)defview_active_todo(self):self.bot.click(self.view_active_by)defview_completed_todo(self):self.bot.click(self.view_completed_by)@pytest.fixturedefpage(chrome_driver)->TodoPage:driver=chrome_driverreturnTodoPage(driver).get()classTestTodoPage:deftest_new_todo(self,page:TodoPage):assertpage.todo_count()==0page.new_todo("aaa")assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_toggle(self,page:TodoPage):s="aaa"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(0)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_delete(self,page:TodoPage):s1="aaa"s2="bbb"page.new_todo(s1)page.new_todo(s2)assertpage.count_todo_items_left()==page.build_count_todo_left(2)page.delete_todo(s1)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.delete_todo(s2)assertpage.todo_count()==0deftest_new_100_todo(self,page:TodoPage):foriinrange(100):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(100)deftest_toggle_all_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(0)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10deftest_clear_completed_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10foriinrange(5):s=f"ToDo{i}"page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==10page.clear_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==5deftest_view_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)foriinrange(4):s=f"ToDo{i}"page.toggle_todo(s)page.view_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==10page.view_active_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==6page.view_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==4
"""
An example of `python + pytest + selenium`
which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
"""importpytestfromseleniumimportwebdriverfromselenium.commonimport(ElementNotInteractableException,NoSuchElementException,StaleElementReferenceException,)fromselenium.webdriverimportActionChainsfromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webelementimportWebElementfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait@pytest.fixture(scope="function")defchrome_driver():withwebdriver.Chrome()asdriver:driver.set_window_size(1024,768)driver.implicitly_wait(0.5)yielddriverclassActionBot:def__init__(self,driver)->None:self.driver=driverself.wait=WebDriverWait(driver,timeout=10,poll_frequency=2,ignored_exceptions=[NoSuchElementException,StaleElementReferenceException,ElementNotInteractableException,],)defelement(self,locator:tuple)->WebElement:self.wait.until(lambdadriver:driver.find_element(*locator))returnself.driver.find_element(*locator)defelements(self,locator:tuple)->list[WebElement]:returnself.driver.find_elements(*locator)defhover(self,locator:tuple)->None:element=self.element(locator)ActionChains(self.driver).move_to_element(element).perform()defclick(self,locator:tuple)->None:element=self.element(locator)element.click()deftype(self,locator:tuple,value:str)->None:element=self.element(locator)element.clear()element.send_keys(value)deftext(self,locator:tuple)->str:element=self.element(locator)returnelement.textclassLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnselfclassTodoPage(LoadableComponent):url="https://todomvc.com/examples/react/dist/"new_todo_by=(By.CSS_SELECTOR,"input.new-todo")count_todo_left_by=(By.CSS_SELECTOR,"span.todo-count")todo_items_by=(By.CSS_SELECTOR,"ul.todo-list>li")view_all_by=(By.LINK_TEXT,"All")view_active_by=(By.LINK_TEXT,"Active")view_completed_by=(By.LINK_TEXT,"Completed")toggle_all_by=(By.CSS_SELECTOR,"input.toggle-all")clear_completed_by=(By.CSS_SELECTOR,"button.clear-completed")@staticmethoddefbuild_todo_by(s:str)->tuple:p=f"//li[.//label[contains(text(), '{s}')]]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_label_by(s:str)->tuple:p=f"//label[contains(text(), '{s}')]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_toggle_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../input[@class='toggle']"returnby,p@staticmethoddefbuild_todo_item_delete_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../button[@class='destroy']"returnby,pdefbuild_count_todo_left(self,count:int)->str:ifcount==1:return"1 item left!"else:returnf"{count} items left!"def__init__(self,driver):self.driver=driverself.bot=ActionBot(driver)defload(self):self.driver.get(self.url)defis_loaded(self):try:WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.new_todo_by))returnTrueexcept:returnFalse# business domain belowdefcount_todo_items_left(self)->str:returnself.bot.text(self.count_todo_left_by)deftodo_count(self)->int:returnlen(self.bot.elements(self.todo_items_by))defnew_todo(self,s:str):self.bot.type(self.new_todo_by,s+"\n")deftoggle_todo(self,s:str):self.bot.click(self.build_todo_item_toggle_by(s))defhover_todo(self,s:str)->None:self.bot.hover(self.build_todo_by(s))defdelete_todo(self,s:str):self.hover_todo(s)self.bot.click(self.build_todo_item_delete_by(s))defclear_completed_todo(self):self.bot.click(self.clear_completed_by)deftoggle_all_todo(self):self.bot.click(self.toggle_all_by)defview_all_todo(self):self.bot.click(self.view_all_by)defview_active_todo(self):self.bot.click(self.view_active_by)defview_completed_todo(self):self.bot.click(self.view_completed_by)@pytest.fixturedefpage(chrome_driver)->TodoPage:driver=chrome_driverreturnTodoPage(driver).get()classTestTodoPage:deftest_new_todo(self,page:TodoPage):assertpage.todo_count()==0page.new_todo("aaa")assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_toggle(self,page:TodoPage):s="aaa"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(0)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_delete(self,page:TodoPage):s1="aaa"s2="bbb"page.new_todo(s1)page.new_todo(s2)assertpage.count_todo_items_left()==page.build_count_todo_left(2)page.delete_todo(s1)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.delete_todo(s2)assertpage.todo_count()==0deftest_new_100_todo(self,page:TodoPage):foriinrange(100):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(100)deftest_toggle_all_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(0)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10deftest_clear_completed_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10foriinrange(5):s=f"ToDo{i}"page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==10page.clear_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==5deftest_view_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)foriinrange(4):s=f"ToDo{i}"page.toggle_todo(s)page.view_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==10page.view_active_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==6page.view_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==4
classLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnself
"""
An example of `python + pytest + selenium`
which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
"""importpytestfromseleniumimportwebdriverfromselenium.commonimport(ElementNotInteractableException,NoSuchElementException,StaleElementReferenceException,)fromselenium.webdriverimportActionChainsfromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webelementimportWebElementfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait@pytest.fixture(scope="function")defchrome_driver():withwebdriver.Chrome()asdriver:driver.set_window_size(1024,768)driver.implicitly_wait(0.5)yielddriverclassActionBot:def__init__(self,driver)->None:self.driver=driverself.wait=WebDriverWait(driver,timeout=10,poll_frequency=2,ignored_exceptions=[NoSuchElementException,StaleElementReferenceException,ElementNotInteractableException,],)defelement(self,locator:tuple)->WebElement:self.wait.until(lambdadriver:driver.find_element(*locator))returnself.driver.find_element(*locator)defelements(self,locator:tuple)->list[WebElement]:returnself.driver.find_elements(*locator)defhover(self,locator:tuple)->None:element=self.element(locator)ActionChains(self.driver).move_to_element(element).perform()defclick(self,locator:tuple)->None:element=self.element(locator)element.click()deftype(self,locator:tuple,value:str)->None:element=self.element(locator)element.clear()element.send_keys(value)deftext(self,locator:tuple)->str:element=self.element(locator)returnelement.textclassLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnselfclassTodoPage(LoadableComponent):url="https://todomvc.com/examples/react/dist/"new_todo_by=(By.CSS_SELECTOR,"input.new-todo")count_todo_left_by=(By.CSS_SELECTOR,"span.todo-count")todo_items_by=(By.CSS_SELECTOR,"ul.todo-list>li")view_all_by=(By.LINK_TEXT,"All")view_active_by=(By.LINK_TEXT,"Active")view_completed_by=(By.LINK_TEXT,"Completed")toggle_all_by=(By.CSS_SELECTOR,"input.toggle-all")clear_completed_by=(By.CSS_SELECTOR,"button.clear-completed")@staticmethoddefbuild_todo_by(s:str)->tuple:p=f"//li[.//label[contains(text(), '{s}')]]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_label_by(s:str)->tuple:p=f"//label[contains(text(), '{s}')]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_toggle_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../input[@class='toggle']"returnby,p@staticmethoddefbuild_todo_item_delete_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../button[@class='destroy']"returnby,pdefbuild_count_todo_left(self,count:int)->str:ifcount==1:return"1 item left!"else:returnf"{count} items left!"def__init__(self,driver):self.driver=driverself.bot=ActionBot(driver)defload(self):self.driver.get(self.url)defis_loaded(self):try:WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.new_todo_by))returnTrueexcept:returnFalse# business domain belowdefcount_todo_items_left(self)->str:returnself.bot.text(self.count_todo_left_by)deftodo_count(self)->int:returnlen(self.bot.elements(self.todo_items_by))defnew_todo(self,s:str):self.bot.type(self.new_todo_by,s+"\n")deftoggle_todo(self,s:str):self.bot.click(self.build_todo_item_toggle_by(s))defhover_todo(self,s:str)->None:self.bot.hover(self.build_todo_by(s))defdelete_todo(self,s:str):self.hover_todo(s)self.bot.click(self.build_todo_item_delete_by(s))defclear_completed_todo(self):self.bot.click(self.clear_completed_by)deftoggle_all_todo(self):self.bot.click(self.toggle_all_by)defview_all_todo(self):self.bot.click(self.view_all_by)defview_active_todo(self):self.bot.click(self.view_active_by)defview_completed_todo(self):self.bot.click(self.view_completed_by)@pytest.fixturedefpage(chrome_driver)->TodoPage:driver=chrome_driverreturnTodoPage(driver).get()classTestTodoPage:deftest_new_todo(self,page:TodoPage):assertpage.todo_count()==0page.new_todo("aaa")assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_toggle(self,page:TodoPage):s="aaa"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(0)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_delete(self,page:TodoPage):s1="aaa"s2="bbb"page.new_todo(s1)page.new_todo(s2)assertpage.count_todo_items_left()==page.build_count_todo_left(2)page.delete_todo(s1)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.delete_todo(s2)assertpage.todo_count()==0deftest_new_100_todo(self,page:TodoPage):foriinrange(100):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(100)deftest_toggle_all_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(0)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10deftest_clear_completed_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10foriinrange(5):s=f"ToDo{i}"page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==10page.clear_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==5deftest_view_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)foriinrange(4):s=f"ToDo{i}"page.toggle_todo(s)page.view_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==10page.view_active_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==6page.view_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==4
"""
An example of `python + pytest + selenium`
which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
"""importpytestfromseleniumimportwebdriverfromselenium.commonimport(ElementNotInteractableException,NoSuchElementException,StaleElementReferenceException,)fromselenium.webdriverimportActionChainsfromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webelementimportWebElementfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait@pytest.fixture(scope="function")defchrome_driver():withwebdriver.Chrome()asdriver:driver.set_window_size(1024,768)driver.implicitly_wait(0.5)yielddriverclassActionBot:def__init__(self,driver)->None:self.driver=driverself.wait=WebDriverWait(driver,timeout=10,poll_frequency=2,ignored_exceptions=[NoSuchElementException,StaleElementReferenceException,ElementNotInteractableException,],)defelement(self,locator:tuple)->WebElement:self.wait.until(lambdadriver:driver.find_element(*locator))returnself.driver.find_element(*locator)defelements(self,locator:tuple)->list[WebElement]:returnself.driver.find_elements(*locator)defhover(self,locator:tuple)->None:element=self.element(locator)ActionChains(self.driver).move_to_element(element).perform()defclick(self,locator:tuple)->None:element=self.element(locator)element.click()deftype(self,locator:tuple,value:str)->None:element=self.element(locator)element.clear()element.send_keys(value)deftext(self,locator:tuple)->str:element=self.element(locator)returnelement.textclassLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnselfclassTodoPage(LoadableComponent):url="https://todomvc.com/examples/react/dist/"new_todo_by=(By.CSS_SELECTOR,"input.new-todo")count_todo_left_by=(By.CSS_SELECTOR,"span.todo-count")todo_items_by=(By.CSS_SELECTOR,"ul.todo-list>li")view_all_by=(By.LINK_TEXT,"All")view_active_by=(By.LINK_TEXT,"Active")view_completed_by=(By.LINK_TEXT,"Completed")toggle_all_by=(By.CSS_SELECTOR,"input.toggle-all")clear_completed_by=(By.CSS_SELECTOR,"button.clear-completed")@staticmethoddefbuild_todo_by(s:str)->tuple:p=f"//li[.//label[contains(text(), '{s}')]]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_label_by(s:str)->tuple:p=f"//label[contains(text(), '{s}')]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_toggle_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../input[@class='toggle']"returnby,p@staticmethoddefbuild_todo_item_delete_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../button[@class='destroy']"returnby,pdefbuild_count_todo_left(self,count:int)->str:ifcount==1:return"1 item left!"else:returnf"{count} items left!"def__init__(self,driver):self.driver=driverself.bot=ActionBot(driver)defload(self):self.driver.get(self.url)defis_loaded(self):try:WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.new_todo_by))returnTrueexcept:returnFalse# business domain belowdefcount_todo_items_left(self)->str:returnself.bot.text(self.count_todo_left_by)deftodo_count(self)->int:returnlen(self.bot.elements(self.todo_items_by))defnew_todo(self,s:str):self.bot.type(self.new_todo_by,s+"\n")deftoggle_todo(self,s:str):self.bot.click(self.build_todo_item_toggle_by(s))defhover_todo(self,s:str)->None:self.bot.hover(self.build_todo_by(s))defdelete_todo(self,s:str):self.hover_todo(s)self.bot.click(self.build_todo_item_delete_by(s))defclear_completed_todo(self):self.bot.click(self.clear_completed_by)deftoggle_all_todo(self):self.bot.click(self.toggle_all_by)defview_all_todo(self):self.bot.click(self.view_all_by)defview_active_todo(self):self.bot.click(self.view_active_by)defview_completed_todo(self):self.bot.click(self.view_completed_by)@pytest.fixturedefpage(chrome_driver)->TodoPage:driver=chrome_driverreturnTodoPage(driver).get()classTestTodoPage:deftest_new_todo(self,page:TodoPage):assertpage.todo_count()==0page.new_todo("aaa")assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_toggle(self,page:TodoPage):s="aaa"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(0)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_delete(self,page:TodoPage):s1="aaa"s2="bbb"page.new_todo(s1)page.new_todo(s2)assertpage.count_todo_items_left()==page.build_count_todo_left(2)page.delete_todo(s1)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.delete_todo(s2)assertpage.todo_count()==0deftest_new_100_todo(self,page:TodoPage):foriinrange(100):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(100)deftest_toggle_all_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(0)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10deftest_clear_completed_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10foriinrange(5):s=f"ToDo{i}"page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==10page.clear_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==5deftest_view_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)foriinrange(4):s=f"ToDo{i}"page.toggle_todo(s)page.view_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==10page.view_active_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==6page.view_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==4
"""
An example of `python + pytest + selenium`
which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
"""importpytestfromseleniumimportwebdriverfromselenium.commonimport(ElementNotInteractableException,NoSuchElementException,StaleElementReferenceException,)fromselenium.webdriverimportActionChainsfromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webelementimportWebElementfromselenium.webdriver.supportimportexpected_conditionsasECfromselenium.webdriver.support.uiimportWebDriverWait@pytest.fixture(scope="function")defchrome_driver():withwebdriver.Chrome()asdriver:driver.set_window_size(1024,768)driver.implicitly_wait(0.5)yielddriverclassActionBot:def__init__(self,driver)->None:self.driver=driverself.wait=WebDriverWait(driver,timeout=10,poll_frequency=2,ignored_exceptions=[NoSuchElementException,StaleElementReferenceException,ElementNotInteractableException,],)defelement(self,locator:tuple)->WebElement:self.wait.until(lambdadriver:driver.find_element(*locator))returnself.driver.find_element(*locator)defelements(self,locator:tuple)->list[WebElement]:returnself.driver.find_elements(*locator)defhover(self,locator:tuple)->None:element=self.element(locator)ActionChains(self.driver).move_to_element(element).perform()defclick(self,locator:tuple)->None:element=self.element(locator)element.click()deftype(self,locator:tuple,value:str)->None:element=self.element(locator)element.clear()element.send_keys(value)deftext(self,locator:tuple)->str:element=self.element(locator)returnelement.textclassLoadableComponent:defload(self):raiseNotImplementedError("Subclasses must implement this method")defis_loaded(self):raiseNotImplementedError("Subclasses must implement this method")defget(self):ifnotself.is_loaded():self.load()ifnotself.is_loaded():raiseException("Page not loaded properly.")returnselfclassTodoPage(LoadableComponent):url="https://todomvc.com/examples/react/dist/"new_todo_by=(By.CSS_SELECTOR,"input.new-todo")count_todo_left_by=(By.CSS_SELECTOR,"span.todo-count")todo_items_by=(By.CSS_SELECTOR,"ul.todo-list>li")view_all_by=(By.LINK_TEXT,"All")view_active_by=(By.LINK_TEXT,"Active")view_completed_by=(By.LINK_TEXT,"Completed")toggle_all_by=(By.CSS_SELECTOR,"input.toggle-all")clear_completed_by=(By.CSS_SELECTOR,"button.clear-completed")@staticmethoddefbuild_todo_by(s:str)->tuple:p=f"//li[.//label[contains(text(), '{s}')]]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_label_by(s:str)->tuple:p=f"//label[contains(text(), '{s}')]"returnBy.XPATH,p@staticmethoddefbuild_todo_item_toggle_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../input[@class='toggle']"returnby,p@staticmethoddefbuild_todo_item_delete_by(s:str)->tuple:by,using=TodoPage.build_todo_item_label_by(s)p=f"{using}/../button[@class='destroy']"returnby,pdefbuild_count_todo_left(self,count:int)->str:ifcount==1:return"1 item left!"else:returnf"{count} items left!"def__init__(self,driver):self.driver=driverself.bot=ActionBot(driver)defload(self):self.driver.get(self.url)defis_loaded(self):try:WebDriverWait(self.driver,10).until(EC.visibility_of_element_located(self.new_todo_by))returnTrueexcept:returnFalse# business domain belowdefcount_todo_items_left(self)->str:returnself.bot.text(self.count_todo_left_by)deftodo_count(self)->int:returnlen(self.bot.elements(self.todo_items_by))defnew_todo(self,s:str):self.bot.type(self.new_todo_by,s+"\n")deftoggle_todo(self,s:str):self.bot.click(self.build_todo_item_toggle_by(s))defhover_todo(self,s:str)->None:self.bot.hover(self.build_todo_by(s))defdelete_todo(self,s:str):self.hover_todo(s)self.bot.click(self.build_todo_item_delete_by(s))defclear_completed_todo(self):self.bot.click(self.clear_completed_by)deftoggle_all_todo(self):self.bot.click(self.toggle_all_by)defview_all_todo(self):self.bot.click(self.view_all_by)defview_active_todo(self):self.bot.click(self.view_active_by)defview_completed_todo(self):self.bot.click(self.view_completed_by)@pytest.fixturedefpage(chrome_driver)->TodoPage:driver=chrome_driverreturnTodoPage(driver).get()classTestTodoPage:deftest_new_todo(self,page:TodoPage):assertpage.todo_count()==0page.new_todo("aaa")assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_toggle(self,page:TodoPage):s="aaa"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(0)page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(1)deftest_todo_delete(self,page:TodoPage):s1="aaa"s2="bbb"page.new_todo(s1)page.new_todo(s2)assertpage.count_todo_items_left()==page.build_count_todo_left(2)page.delete_todo(s1)assertpage.count_todo_items_left()==page.build_count_todo_left(1)page.delete_todo(s2)assertpage.todo_count()==0deftest_new_100_todo(self,page:TodoPage):foriinrange(100):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(100)deftest_toggle_all_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(0)assertpage.todo_count()==10page.toggle_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10deftest_clear_completed_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(10)assertpage.todo_count()==10foriinrange(5):s=f"ToDo{i}"page.toggle_todo(s)assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==10page.clear_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(5)assertpage.todo_count()==5deftest_view_todo(self,page:TodoPage):foriinrange(10):s=f"ToDo{i}"page.new_todo(s)foriinrange(4):s=f"ToDo{i}"page.toggle_todo(s)page.view_all_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==10page.view_active_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==6page.view_completed_todo()assertpage.count_todo_items_left()==page.build_count_todo_left(6)assertpage.todo_count()==4