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 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.
Example
An example of `python + pytest + selenium` which implemented "**Action Bot**, **Loadable Component** and **Page Object**".
A `pytest` fixture `chrome_driver`.
"""
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