├── .github └── workflows │ └── codeql-analysis.yml ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── _static │ └── css │ │ └── custom.css ├── conf.py ├── index.rst ├── new_logo.JPG ├── requirements.txt └── selenium-page-factory_logo.png ├── selenium-page-factory_logo.png ├── seleniumpagefactory ├── Pagefactory.py └── __init__.py ├── setup.py └── tests ├── Factory_approch.py ├── __init__.py ├── selenium_unitest.py └── test_selenium.py /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '43 22 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Set the OS, Python version and other tools you might need 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.12" 6 | 7 | # Build documentation in the "docs/" directory with Sphinx 8 | # sphinx: 9 | # configuration: docs/conf.py 10 | 11 | # Optional but recommended, declare the Python requirements required 12 | # to build your documentation 13 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Sujit Nayakwadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | selenium-page-factory 2 | ===================== 3 | 4 | 5 | 6 | Python library provides page factory approach to implement page object model in selenium 7 | 8 | ![Python](https://img.shields.io/badge/python-v3.7+-blue.svg) 9 | [![Pypi](https://img.shields.io/badge/Pypi-v2.6-green)](https://pypi.org/project/selenium-page-factory/) 10 | ![Dependencies](https://img.shields.io/badge/dependencies-up%20to%20date-brightgreen.svg) 11 | ![License](https://img.shields.io/pypi/l/selenium-wire.svg) 12 | [![Documentation](https://img.shields.io/badge/Documantation-latest-brightgreen)](https://selenium-page-factory.readthedocs.io/en/latest/) 13 | ![Downloads](https://img.shields.io/pypi/dm/selenium-page-factory) 14 | 15 | Introduction 16 | ============ 17 | 18 | * A Page Factory is one way of implementing a Page Object Model. In order to support the Page Object pattern. 19 | * As in Java we are using @findBy, here we are declaring all web element in dictionary. 20 | Dictionary keys become WebElement / class member variable with having all extended WebElement methods. 21 | 22 | 23 | Main Features 24 | ============= 25 | 26 | * Initialise all the webElements declared in Point at a time. 27 | * All WebElements methods are re-define to add extra features eg- click method extended to have explicit wait for element to be clickable. 28 | * Cent percent unittest coverage. 29 | * Supports Selenium 4 ActionChains methods 30 | * Now Support Appium for mobile testing 31 | * Raised custom Page factory exceptions 32 | 33 | Installation 34 | ============= 35 | 36 | ```shell 37 | pip install selenium-page-factory 38 | ``` 39 | 40 | Pre-Requisite 41 | ============= 42 | Every Page in Page Object Model should have WebDriver object as class member 43 | as shown below 44 | 45 | ```python 46 | class PageClass(PageFactory): 47 | 48 | def __init__(self,driver): 49 | self.driver = driver # Required 50 | self.timeout = 15 #(Optional - Customise your explicit wait for every webElement) 51 | self.highlight = True #(Optional - To highlight every webElement in PageClass) 52 | self.mobile_test = False #(Optional - Added for Appium support) 53 | ``` 54 | 55 | Extended WebElements Methods 56 | =================== 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
set_textget_text
clear_textclick_button
double_clickget_list_item_count
select_element_by_textselect_element_by_index
select_element_by_valueget_all_list_item
get_list_selected_itemhighlight
is_Enabledis_Checked
getAttributehover
visibility_of_element_locatedinvisibility_of_element_located
element_to_be_clickabletext_to_be_present_in_element
context_clickexecute_script
click_and_holdrelease
hover_with_offsetComing soon...
116 | 117 | Note: 118 | Every WebElement will be created after verifying it's Presence and visibility on Page at Run-Time. 119 | 120 | 121 | *[selenium-page-factory Documentation](https://selenium-page-factory.readthedocs.io)* 122 | 123 | Selenium Python Framework Example *[here](https://github.com/NayakwadiS/Selenium_Python_UnitTest_HTML)* 124 | 125 | 126 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search { 2 | /* Rainforest alliance light gray */ 3 | background-color: #325270; 4 | } 5 | 6 | .wy-nav-side { 7 | /* menu */ 8 | background: #456e96; 9 | /* GCP green on brown */ 10 | color: #799d44; 11 | } 12 | .rst-versions .rst-current-version{ 13 | /*footer*/ 14 | background-color: #325270; 15 | } 16 | .wy-alert-title, .rst-content .admonition-title { 17 | /* Note */ 18 | background: #325270; 19 | } 20 | 21 | .wy-nav-content { 22 | max-width: inherit; 23 | } 24 | 25 | .current{ 26 | background: #456e96; 27 | } 28 | 29 | .wy-side-scroll { 30 | width: auto; 31 | overflow-y: auto; 32 | } 33 | 34 | p { 35 | margin-bottom: 0.5rem; 36 | } 37 | 38 | h1, h2, h3, h4, h5 { 39 | margin-bottom: 12px; 40 | } 41 | 42 | #global-coffee-data-standard h2 { 43 | /* Rainforest alliance green */ 44 | border-top: 3px dashed #4e917a; 45 | padding-top: 0.5rem; 46 | margin-top: 1rem; 47 | } 48 | 49 | .caption-text { 50 | font-weight: bold; 51 | } 52 | 53 | 54 | div.wy-table-responsive > table.docutils td { 55 | word-wrap: break-word; 56 | white-space: inherit; 57 | max-width: 20%; 58 | } 59 | 60 | div.wy-table-responsive > table.docutils td:nth-child(3) { 61 | max-width: 40%; 62 | } 63 | 64 | div:not(#contents).topic { 65 | /* Rainforest alliance green */ 66 | border: 1px solid #4e917a; 67 | padding: 3px; 68 | width: 100%; 69 | border-radius: 3px; 70 | } 71 | 72 | div.topic p, 73 | div.topic p.topic-title { 74 | margin-bottom: 0; 75 | } 76 | 77 | div:not(#contents).topic p.topic-title { 78 | display: none; 79 | } 80 | 81 | button.btn-example-data { 82 | background-color: #4e917a !important; 83 | margin-top: 5px; 84 | margin-bottom: 5px; 85 | } 86 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from recommonmark.transform import AutoStructify 2 | from recommonmark.parser import CommonMarkParser 3 | 4 | # -- Project information ------------------------- 5 | 6 | project = 'selenium-page-factory' 7 | 8 | # The short X.Y version 9 | version = '' 10 | # The full version, including alpha/beta/rc tags 11 | release = '2.7' 12 | 13 | # The master toctree document. 14 | master_doc = 'index' 15 | 16 | html_favicon = 'new_logo.JPG' 17 | 18 | html_static_path = ['_static'] 19 | 20 | def setup(app): 21 | app.add_stylesheet('css/custom.css?v20240414') 22 | 23 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | * Python library provides page factory approach to implement page object model in selenium 4 | * A Page Factory is one way of implementing a Page Object Model. In order to support the Page Object pattern. 5 | * As in Java we are using @findBy, here we are declaring all web element in dictionary. 6 | Dictionary keys become WebElement / class member variable with having all extended WebElement methods. 7 | 8 | Github Project Page 9 | =================== 10 | 11 | https://github.com/NayakwadiS/selenium-page-factory 12 | 13 | Main Features 14 | ============= 15 | 16 | * Initialise all the webElements declared in Point at a time. 17 | * All WebElements methods are re-define to add extra features eg- click method extended to have explicit wait for element to be clickable. 18 | * Cent percent unittest coverage. 19 | * Supports Selenium 4 ActionChains methods 20 | * Now Support Appium for mobile testing 21 | * Raised custom Page factory exceptions 22 | 23 | 24 | Installation 25 | ============= 26 | pip install:: 27 | 28 | pip install selenium-page-factory 29 | 30 | Update 31 | =============== 32 | To updated to the lasted version:: 33 | 34 | pip install selenium-page-factory --upgrade 35 | 36 | Pre-Requisite 37 | ============= 38 | Every Page in Page Object Model should have WebDriver object as class member 39 | as shown below:: 40 | 41 | class PageClass(PageFactory): 42 | 43 | def __init__(self,driver): 44 | self.driver = driver 45 | self.timeout = 15 #(Optional - Customise your explicit wait for every webElement) 46 | self.highlight = True #(Optional - To highlight every webElement in PageClass) 47 | self.mobile_test = False #(Optional - Added for Appium support) 48 | 49 | Extended WebElements Methods 50 | ============================ 51 | .. raw:: html 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
set_textget_text
clear_textclick_button
double_clickget_list_item_count
select_element_by_textselect_element_by_index
select_element_by_valueget_all_list_item
get_list_selected_itemhighlight
is_Enabledis_Checked
getAttributehover
visibility_of_element_locatedinvisibility_of_element_located
element_to_be_clickableexecute_script
context_clicktext_to_be_present_in_element
click_and_holdrelease
hover_with_offsetComing soon...
109 | 110 | ============================ 111 | 112 | .. note:: 113 | 114 | Every WebElement will be created after verifying it's Presence and visibility on Page at Run-Time. 115 | 116 | Examples 117 | ============= 118 | 119 | Python - Unittest 120 | -------------- 121 | 122 | Inside test_Login.py:: 123 | 124 | import unittest 125 | from selenium import webdriver 126 | from seleniumpagefactory.Pagefactory import PageFactory 127 | 128 | class LoginPage(PageFactory): 129 | 130 | def __init__(self,driver): 131 | # It is necessary to to initialise driver as page class member to implement Page Factory 132 | self.driver = driver 133 | 134 | # define locators dictionary where key name will became WebElement using PageFactory 135 | locators = { 136 | "edtUserName": ('ID', 'user_login'), 137 | "edtPassword": ('NAME', 'pwd'), 138 | "btnSignIn": ('XPATH', '//input[@value="Log In"]') 139 | } 140 | 141 | def login(self): 142 | # set_text(), click_button() methods are extended methods in PageFactory 143 | self.edtUserName.set_text("") # edtUserName become class variable using PageFactory 144 | self.edtPassword.set_text("") 145 | self.btnSignIn.click_button() 146 | 147 | 148 | class LoginTest(unittest.TestCase): 149 | 150 | def test_Login(self): 151 | driver = webdriver.Chrome() 152 | driver.get("https://s1.demo.opensourcecms.com/wordpress/wp-login.php") 153 | 154 | pglogin = LoginPage(driver) 155 | pglogin.login() 156 | 157 | if __name__ == "__main__": 158 | unittest.main() 159 | 160 | 161 | Python - Pytest 162 | --------------- 163 | 164 | Inside test_Login.py:: 165 | 166 | import pytest 167 | from selenium import webdriver 168 | from seleniumpagefactory.Pagefactory import PageFactory 169 | 170 | def test_Login(): 171 | driver = webdriver.Chrome("") 172 | driver.get("https://s1.demo.opensourcecms.com/wordpress/wp-login.php") 173 | 174 | pglogin = LoginPage(driver) 175 | pglogin.login() 176 | 177 | class LoginPage(PageFactory): 178 | 179 | def __init__(self,driver): 180 | # It is necessary to to initialise driver as page class member to implement Page Factory 181 | self.driver = driver 182 | 183 | # define locators dictionary where key name will became WebElement using PageFactory 184 | locators = { 185 | "edtUserName": ('ID', 'user_login'), 186 | "edtPassword": ('NAME', 'pwd'), 187 | "btnSignIn": ('XPATH', '//input[@value="Log In"]') 188 | "lnkPost": ('XPATH', '//div[contains(text(),"Posts")]'), 189 | "lstAction": ('ID', 'bulk-action-selector-top') 190 | } 191 | 192 | def login(self): 193 | # set_text(), click_button() methods are extended methods in PageFactory 194 | self.edtUserName.set_text("") # edtUserName become class variable using PageFactory 195 | self.edtPassword.set_text("") 196 | self.btnSignIn.click_button() 197 | 198 | WebElement Methods Usage 199 | ========================== 200 | set_text 201 | --------- 202 | To perform set text operation:: 203 | 204 | class LoginPage(PageFactory): 205 | 206 | def login(self): 207 | self.edtUserName.set_text("opensourcecms") 208 | 209 | get_text 210 | --------- 211 | To get text from edit box:: 212 | 213 | class LoginPage(PageFactory): 214 | 215 | def login(self): 216 | text_from_element = self.edtUserName.get_text() 217 | 218 | clear_text 219 | --------- 220 | To clear text from edit box:: 221 | 222 | class LoginPage(PageFactory): 223 | 224 | def login(self): 225 | self.edtUserName.clear_text() 226 | 227 | click_button 228 | ------------- 229 | To Click on any WebElement:: 230 | 231 | class LoginPage(PageFactory): 232 | 233 | def login(self): 234 | self.btnSignIn.click_button() 235 | 236 | click_and_hold 237 | ------------- 238 | To click_and_hold on Element:: 239 | 240 | class LoginPage(PageFactory): 241 | 242 | def login(self): 243 | self.btnSignIn.click_and_hold() 244 | 245 | release 246 | ------------- 247 | Releasing a held mouse button on an element:: 248 | 249 | class LoginPage(PageFactory): 250 | 251 | def login(self): 252 | self.btnSignIn.release() 253 | 254 | 255 | get_list_item_count 256 | ------------------ 257 | Get list item count:: 258 | 259 | class customPage(PageFactory): 260 | 261 | def perform_list_operation(self): 262 | list_item_count = self.lstAction.get_list_item_count() 263 | 264 | select_element_by_text 265 | ---------------------- 266 | To Select list item by using visible text:: 267 | 268 | class customPage(PageFactory): 269 | 270 | def perform_list_operation(self): 271 | self.lstAction.select_element_by_text("India") 272 | 273 | select_element_by_index 274 | ---------------------- 275 | To Select list item by using index:: 276 | 277 | class customPage(PageFactory): 278 | 279 | def perform_list_operation(self): 280 | self.lstAction.select_element_by_index(0) 281 | 282 | select_element_by_value 283 | ---------------------- 284 | To Select list item by using webElement value property:: 285 | 286 | class customPage(PageFactory): 287 | 288 | def perform_list_operation(self): 289 | self.lstAction.select_element_by_value("country India") 290 | 291 | get_all_list_item 292 | ------------------ 293 | Get all list items:: 294 | 295 | class customPage(PageFactory): 296 | 297 | def perform_list_operation(self): 298 | list_items = self.lstAction.get_all_list_item() 299 | 300 | get_list_selected_item 301 | ------------------ 302 | Get selected list item:: 303 | 304 | class customPage(PageFactory): 305 | 306 | def perform_list_operation(self): 307 | selected_list_item = self.lstAction.get_list_selected_item() 308 | 309 | hover 310 | ------------- 311 | To hover on any WebElement:: 312 | 313 | class customPage(PageFactory): 314 | 315 | def login(self): 316 | self.btnSignIn.hover() 317 | 318 | is_Checked 319 | ------------------ 320 | Verify RadioButton and CheckBox:: 321 | 322 | class customPage(PageFactory): 323 | 324 | def checkbox_radiobutton_operation(self): 325 | checkBox_is_selected = self.chkGender.is_Checked() 326 | 327 | is_Enabled 328 | ------------------ 329 | Verify Enable state of WebElemnt:: 330 | 331 | class customPage(PageFactory): 332 | 333 | def checkbox_radiobutton_operation(self): 334 | checkBox_is_enabled = self.chkGender.is_Enabled() 335 | 336 | getAttribute 337 | ------------------ 338 | Get HTML attribute value of WebElemnt:: 339 | 340 | class customPage(PageFactory): 341 | 342 | def link_operation(self): 343 | title_attribute = self.nextLink.getAttribute("title") 344 | -------------------------------------------------------------------------------- /docs/new_logo.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayakwadiS/selenium-page-factory/050cd296587084dfd4a6b6c0ccb0de3813c2277e/docs/new_logo.JPG -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | # via sphinx 3 | babel==2.11.0 4 | # via sphinx 5 | certifi==2024.7.4 6 | # via requests 7 | charset-normalizer==2.1.1 8 | # via requests 9 | docutils==0.19 10 | # via sphinx 11 | idna==3.7 12 | # via requests 13 | imagesize==1.4.1 14 | # via sphinx 15 | jinja2==3.1.6 16 | # via sphinx 17 | markupsafe==2.1.1 18 | # via jinja2 19 | packaging==22.0 20 | # via sphinx 21 | pygments==2.15.0 22 | # via sphinx 23 | pytz==2022.7 24 | # via babel 25 | requests==2.32.0 26 | # via sphinx 27 | snowballstemmer==2.2.0 28 | # via sphinx 29 | sphinx==5.3.0 30 | # via -r docs.in 31 | sphinxcontrib-applehelp==1.0.2 32 | # via sphinx 33 | sphinxcontrib-devhelp==1.0.2 34 | # via sphinx 35 | sphinxcontrib-htmlhelp==2.0.0 36 | # via sphinx 37 | sphinxcontrib-jsmath==1.0.1 38 | # via sphinx 39 | sphinxcontrib-qthelp==1.0.3 40 | # via sphinx 41 | sphinxcontrib-serializinghtml==1.1.5 42 | # via sphinx 43 | urllib3==1.26.19 44 | # via requests 45 | recommonmark 46 | -------------------------------------------------------------------------------- /docs/selenium-page-factory_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayakwadiS/selenium-page-factory/050cd296587084dfd4a6b6c0ccb0de3813c2277e/docs/selenium-page-factory_logo.png -------------------------------------------------------------------------------- /selenium-page-factory_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayakwadiS/selenium-page-factory/050cd296587084dfd4a6b6c0ccb0de3813c2277e/selenium-page-factory_logo.png -------------------------------------------------------------------------------- /seleniumpagefactory/Pagefactory.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.ui import WebDriverWait 2 | from selenium.webdriver.common.by import By 3 | from selenium.webdriver.support import expected_conditions as EC 4 | from selenium.common.exceptions import * 5 | from selenium.webdriver import ActionChains 6 | from selenium.webdriver.remote.webelement import WebElement 7 | from selenium.webdriver.support.ui import Select 8 | 9 | 10 | class PageFactoryException(Exception): 11 | """Base class for exceptions in this module.""" 12 | pass 13 | 14 | 15 | class ElementNotFoundException(PageFactoryException): 16 | """Raised when the element is not found in the page. (Using ` EC.presence_of_element_located` function from within __getattr__ method).""" 17 | 18 | 19 | class ElementNotVisibleException(PageFactoryException): 20 | """Raised when the element is not visible in the page. (Using `EC.visibility_of_element_located` function from within __getattr__ method).""" 21 | 22 | 23 | class PageFactory(object): 24 | timeout = 10 25 | highlight = False 26 | mobile_test = False # Added for Mobile support 27 | 28 | TYPE_OF_LOCATORS = { 29 | 'css': By.CSS_SELECTOR, 30 | 'id': By.ID, 31 | 'name': By.NAME, 32 | 'xpath': By.XPATH, 33 | 'link_text': By.LINK_TEXT, 34 | 'partial_link_text': By.PARTIAL_LINK_TEXT, 35 | 'tag': By.TAG_NAME, 36 | 'class_name': By.CLASS_NAME 37 | } 38 | 39 | def __init__(self): 40 | pass 41 | 42 | def __get__(self, instance, owner): 43 | if not instance: 44 | return None 45 | else: 46 | self.driver = instance.driver 47 | 48 | def __getattr__(self, loc): 49 | 50 | if self.mobile_test == True: 51 | if loc in self.locators.keys(): 52 | element = self.find_element_by_accessibility_id(self.locators[loc][1]) 53 | return element 54 | else: 55 | if loc in self.locators.keys(): 56 | locator = (self.TYPE_OF_LOCATORS[self.locators[loc][0].lower()], self.locators[loc][1]) 57 | try: 58 | element = WebDriverWait(self.driver, self.timeout).until( 59 | EC.presence_of_element_located(locator) 60 | ) 61 | except (StaleElementReferenceException, NoSuchElementException, TimeoutException) as e: 62 | raise ElementNotFoundException( 63 | "An exception of type " + type(e).__name__ + 64 | " occurred. With Element -: " + loc + 65 | " - locator: (" + locator[0] + ", " + locator[1] + ")" 66 | ) 67 | 68 | try: 69 | element = WebDriverWait(self.driver, self.timeout).until( 70 | EC.visibility_of_element_located(locator) 71 | ) 72 | except (StaleElementReferenceException, NoSuchElementException, TimeoutException) as e: 73 | raise ElementNotVisibleException( 74 | "An exception of type " + type(e).__name__ + 75 | " occurred. With Element -: " + loc + 76 | " - locator: (" + locator[0] + ", " + locator[1] + ")" 77 | ) 78 | 79 | element = self.get_web_element(*locator) 80 | element._locator = locator 81 | return element 82 | return super().__getattr__(loc) 83 | 84 | def get_web_element(self, *loc): 85 | element = self.driver.find_element(*loc) 86 | self.highlight_web_element(element) 87 | return element 88 | 89 | def highlight_web_element(self, element): 90 | """ 91 | To highlight webElement 92 | :param: WebElement 93 | :return: None 94 | """ 95 | if self.highlight: 96 | self.driver.execute_script("arguments[0].style.border='2px ridge #33ffff'", element) 97 | 98 | def select_element_by_text(self, text): 99 | """ 100 | Select webElement from dropdown list 101 | :param: Text of Item in dropdown 102 | :return: None 103 | """ 104 | select = Select(self) 105 | select.select_by_visible_text(text) 106 | 107 | def select_element_by_index(self, index): 108 | """ 109 | Select webElement from dropdown list 110 | :param: Index of Item in dropdown 111 | :return: None 112 | """ 113 | select = Select(self) 114 | select.select_by_index(index) 115 | 116 | def select_element_by_value(self, value): 117 | """ 118 | Select webElement from dropdown list 119 | :param: value of Item in dropdown 120 | :return: None 121 | """ 122 | select = Select(self) 123 | select.select_by_value(value) 124 | 125 | def get_list_item_count(self): 126 | """ 127 | Count of Item from Dropdown 128 | :param: None 129 | :return: count 130 | """ 131 | select = Select(self) 132 | return len(select.options) 133 | 134 | def get_all_list_item(self): 135 | """ 136 | Get list of Item from Dropdown 137 | :param: None 138 | :return: list of items present in dropdown 139 | """ 140 | select = Select(self) 141 | list_item = [] 142 | for item in select.options: 143 | list_item.append(item.text) 144 | return list_item 145 | 146 | def get_list_selected_item(self): 147 | """ 148 | Get list of Selected item in Dropdown 149 | :param: None 150 | :return: list of items selected in dropdown 151 | """ 152 | select = Select(self) 153 | list_item = [] 154 | for item in select.all_selected_options: 155 | list_item.append(item.text) 156 | return list_item 157 | 158 | def verify_list_item(self, text): 159 | """ 160 | Verify text to be present in Dropdown 161 | :param: item to be verify 162 | :return: True / False 163 | """ 164 | select = Select(self) 165 | list_item = [] 166 | for item in select.options: 167 | if text == item.text: 168 | return True 169 | return False 170 | 171 | def click_button(self): 172 | """ 173 | Perform click on webElement 174 | :param: None 175 | :return: webElement 176 | """ 177 | self.element_to_be_clickable() 178 | self.click() 179 | return self 180 | 181 | def double_click(self): 182 | """ 183 | perform Double click on webElement 184 | :param: None 185 | :return: webElement 186 | """ 187 | self.element_to_be_clickable() 188 | ActionChains(self.parent).double_click(self).perform() 189 | return self 190 | 191 | def context_click(self): 192 | """ 193 | perform Right click on webElement 194 | Added support for Selenium 4 195 | :param: None 196 | :return: webElement 197 | """ 198 | self.element_to_be_clickable() 199 | ActionChains(self.parent).context_click(on_element=self) 200 | return self 201 | 202 | def click_and_hold(self): 203 | """ 204 | This method will replace the moveToElement(onElement).clickAndHold() 205 | Added support for Selenium 4 206 | :param: None 207 | :return: webElement 208 | """ 209 | self.element_to_be_clickable() 210 | ActionChains(self.parent).click_and_hold(on_element=self) 211 | return self 212 | 213 | def release(self): 214 | """ 215 | Releasing a held mouse button on an element. 216 | Added support for Selenium 4 217 | :param: None 218 | :return: webElement 219 | """ 220 | ActionChains(self.parent).release(on_element=self) 221 | return self 222 | 223 | def hover_with_offset(self, x, y): 224 | """ 225 | Move the mouse by an offset of the specified element. Offsets are relative to the top-left corner of the element. 226 | Added support for Selenium 4 227 | :param: x and y offset 228 | :return: webElement 229 | """ 230 | ActionChains(self.parent).move_to_element_with_offset(to_element=self, xoffset=x, yoffset=y) 231 | return self 232 | 233 | def set_text(self, value): 234 | """ 235 | type text in input box 236 | :param: Text to be Enter 237 | :return: webElement 238 | """ 239 | self.element_to_be_clickable() 240 | self.send_keys(value) 241 | return self 242 | 243 | def get_text(self): 244 | """ 245 | get text from input box 246 | :param: None 247 | :return: text from webElement 248 | """ 249 | return self.text 250 | 251 | def clear_text(self): 252 | """ 253 | Clear text from EditBox 254 | :param: None 255 | :return: None 256 | """ 257 | self.clear() 258 | 259 | def hover(self): 260 | """ 261 | perform hover operation on webElement 262 | :param: None 263 | :return: None 264 | """ 265 | ActionChains(self.parent).move_to_element(self).perform() 266 | 267 | def is_Checked(self): 268 | """ 269 | Check Radio button / CheckBox is selected 270 | :param: None 271 | :return: Boolean 272 | """ 273 | return self.isSelected() 274 | 275 | def is_Enabled(self): 276 | """ 277 | get Enable state of webElement 278 | :param: None 279 | :return: Boolean 280 | """ 281 | return self.isEnabled() 282 | 283 | def getAttribute(self, attributeName): 284 | """ 285 | get webElement attribute 286 | :param: name of Attribute 287 | :return: webElement attribute value 288 | """ 289 | return self.get_attribute(attributeName) 290 | 291 | def setAttribute(self, attributeName, value): 292 | """ 293 | set webElement attribute 294 | :param: name of Attribute 295 | :return: webElement attribute value 296 | """ 297 | return self.parent.execute_script("arguments[0].setAttribute(" + attributeName + "," + value + ")", self) 298 | 299 | def w3c(self): 300 | return self.w3c 301 | 302 | def element_to_be_clickable(self, timeout=None): 303 | """ 304 | Wait till the element to be clickable 305 | """ 306 | if timeout is None: 307 | timeout = PageFactory().timeout 308 | return WebDriverWait(self.parent, timeout).until( 309 | EC.element_to_be_clickable(self._locator) 310 | ) 311 | 312 | def invisibility_of_element_located(self, timeout=None): 313 | """ 314 | Wait till the element to be invisible 315 | """ 316 | if timeout is None: 317 | timeout = PageFactory().timeout 318 | return WebDriverWait(self.parent, timeout).until( 319 | EC.invisibility_of_element_located(self._locator) 320 | ) 321 | 322 | def visibility_of_element_located(self, timeout=None): 323 | """ 324 | Wait till the element to be visible 325 | """ 326 | if timeout is None: 327 | timeout = PageFactory().timeout 328 | return WebDriverWait(self.parent, timeout).until( 329 | EC.visibility_of(self) 330 | ) 331 | 332 | def execute_script(self, script): 333 | """ 334 | Execute JavaScript using web driver on selected web element 335 | :param: Javascript to be execute 336 | :return: None / depends on Script 337 | """ 338 | return self.parent.execute_script(script, self) 339 | 340 | 341 | WebElement.click_button = PageFactory.click_button 342 | WebElement.double_click = PageFactory.double_click 343 | WebElement.context_click = PageFactory.context_click 344 | WebElement.click_and_hold = PageFactory.click_and_hold 345 | WebElement.release = PageFactory.release 346 | WebElement.hover_with_offset = PageFactory.hover_with_offset 347 | WebElement.element_to_be_clickable = PageFactory.element_to_be_clickable 348 | WebElement.invisibility_of_element_located = PageFactory.invisibility_of_element_located 349 | WebElement.visibility_of_element_located = PageFactory.visibility_of_element_located 350 | WebElement.set_text = PageFactory.set_text 351 | WebElement.get_text = PageFactory.get_text 352 | WebElement.hover = PageFactory.hover 353 | WebElement.clear_text = PageFactory.clear_text 354 | WebElement.w3c = PageFactory.w3c 355 | WebElement.is_Checked = PageFactory.is_Checked 356 | WebElement.is_Enabled = PageFactory.is_Enabled 357 | WebElement.getAttribute = PageFactory.getAttribute 358 | WebElement.setAttribute = PageFactory.setAttribute 359 | WebElement.select_element_by_text = PageFactory.select_element_by_text 360 | WebElement.select_element_by_index = PageFactory.select_element_by_index 361 | WebElement.select_element_by_value = PageFactory.select_element_by_value 362 | WebElement.get_list_item_count = PageFactory.get_list_item_count 363 | WebElement.get_all_list_item = PageFactory.get_all_list_item 364 | WebElement.get_list_selected_item = PageFactory.get_list_selected_item 365 | WebElement.execute_script = PageFactory.execute_script 366 | WebElement.verify_list_item = PageFactory.verify_list_item 367 | -------------------------------------------------------------------------------- /seleniumpagefactory/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | Copyright (c) 2019 Sujit Nayakwadi 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. 19 | """ 20 | __VERSION__='0.1' 21 | from seleniumpagefactory.Pagefactory import PageFactory 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, Extension ,find_packages 2 | from os import path 3 | 4 | this_directory = path.abspath(path.dirname(__file__)) 5 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="selenium-page-factory", 10 | version="2.6", 11 | author = "Sujit Nayakwadi", 12 | author_email="nayakwadi_sujit@rediffmail.com", 13 | description="Python library provides page factory approach to implement page object model in selenium", 14 | license="MIT", 15 | keywords="selenium, page object model, pom, pages, page factory", 16 | install_requires=['selenium'], 17 | url="https://github.com/NayakwadiS/selenium-page-factory", 18 | packages=find_packages(), 19 | long_description = long_description, 20 | long_description_content_type='text/markdown', 21 | tests_require=["pytest"], 22 | ) 23 | -------------------------------------------------------------------------------- /tests/Factory_approch.py: -------------------------------------------------------------------------------- 1 | from seleniumpagefactory.Pagefactory import PageFactory 2 | import unittest 3 | from os import environ 4 | 5 | from selenium import webdriver 6 | 7 | class LoginPage(PageFactory): 8 | 9 | def __init__(self,driver): 10 | # It is necessary to to initialise driver as page class member to implement Page Factory 11 | self.driver = driver 12 | 13 | # define locators dictionary where key name will became WebElement using PageFactory 14 | locators = { 15 | "edtUserName": ('ID', 'user_login'), 16 | "edtPassword": ('NAME', 'pwd'), 17 | "btnSignIn": ('XPATH', '//input[@value="Log In"]'), 18 | "lnkPost": ('XPATH','//div[contains(text(),"Posts")]'), 19 | "lstAction": ('ID','bulk-action-selector-top') 20 | } 21 | 22 | def login(self): 23 | # set_text(), click_button() methods are extended methods in PageFactory 24 | self.edtUserName.set_text("opensourcecms") # edtUserName become class variable using PageFactory 25 | self.edtPassword.set_text("opensourcecms") 26 | self.btnSignIn.click_button() 27 | 28 | 29 | class LoginTest(unittest.TestCase): 30 | 31 | def test_Login(self): 32 | driver = webdriver.Chrome("D:\AutomationTool\Resources\drivers\chromedriver.exe") 33 | driver.get("https://s1.demo.opensourcecms.com/wordpress/wp-login.php") 34 | pglogin = LoginPage(driver) 35 | pglogin.edtUserName.set_text("opensourcecms") # edtUserName become class variable using PageFactory 36 | pglogin.edtPassword.set_text("opensourcecms") 37 | pglogin.btnSignIn.click_button() 38 | pglogin.lnkPost.click_button() 39 | print(pglogin.lstAction.get_all_list_item()) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from seleniumpagefactory.Pagefactory import PageFactory -------------------------------------------------------------------------------- /tests/selenium_unitest.py: -------------------------------------------------------------------------------- 1 | from seleniumpagefactory.Pagefactory import PageFactory 2 | import pytest 3 | from selenium import webdriver 4 | 5 | class LoginPage(PageFactory): 6 | 7 | def __init__(self,driver): 8 | # It is necessary to to initialise driver as page class member to implement Page Factory 9 | self.driver =driver 10 | 11 | # define locators dictionary where key name will became WebElement using PageFactory 12 | locators = { 13 | "edtUserName": ('ID', 'user_login'), 14 | "edtPassword": ('NAME', 'pwd'), 15 | "btnSignIn": ('XPATH', '//input[@value="Log In"]') 16 | } 17 | 18 | def login(self): 19 | # set_text(), click_button() methods are extended methods in PageFactory 20 | self.edtUserName.set_text("opensourcecms") # edtUserName become class variable using PageFactory 21 | self.edtPassword.set_text("opensourcecms") 22 | self.btnSignIn.click_button() 23 | 24 | 25 | def test_Login(self): 26 | driver = webdriver.Chrome("path for driver exe") 27 | driver.maximize_window() 28 | driver.implicitly_wait(5) 29 | driver.get("https://s1.demo.opensourcecms.com/wordpress/wp-login.php") 30 | 31 | pglogin = LoginPage(driver) 32 | pglogin.login() 33 | -------------------------------------------------------------------------------- /tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | mport pytest 2 | from selenium import webdriver 3 | from seleniumpagefactory.Pagefactory import PageFactory 4 | 5 | @pytest.fixture 6 | def wp(): 7 | driver = webdriver.Chrome() 8 | driver.maximize_window() 9 | driver.get("https://s1.demo.opensourcecms.com/wordpress/wp-login.php") 10 | try: 11 | yield driver 12 | finally: 13 | driver.close() 14 | 15 | 16 | class LoginPage(PageFactory): 17 | 18 | def __init__(self,driver): 19 | # It is necessary to to initialise driver as page class member to implement Page Factory 20 | self.driver =driver 21 | 22 | # define locators dictionary where key name will became WebElement using PageFactory 23 | locators = { 24 | "edtUserName": ('ID', 'user_login'), 25 | "edtPassword": ('NAME', 'pwd'), 26 | "btnSignIn": ('XPATH', '//input[@value="Log In"]'), 27 | "edtUserLoginCssSelector": ('CSS', "#user_login"), 28 | "unknownElement": ("CSS", "somethingWrong"), 29 | } 30 | 31 | def login(self): 32 | # set_text(), click_button() methods are extended methods in PageFactory 33 | self.edtUserName.set_text("opensourcecms") # edtUserName become class variable using PageFactory 34 | self.edtPassword.set_text("opensourcecms") 35 | self.btnSignIn.click_button() 36 | 37 | 38 | def test_Login(wp): 39 | pglogin = LoginPage(wp) 40 | pglogin.login() 41 | 42 | 43 | def test_css_selector_call_twice(wp): 44 | pglogin = LoginPage(wp) 45 | pglogin.edtUserLoginCssSelector.set_text("test") 46 | # CSS selector were not working as expected while calling a CSS SELECTOR twice 47 | # because By.CSS_SELECTOR return a string 'css selector' which is 48 | # not present in TYPE_OF_LOCATORS, also getattr were altering locators 49 | # attribue 50 | pglogin.edtUserLoginCssSelector.set_text("test2") 51 | 52 | 53 | def test_not_found_element(wp): 54 | with pytest.raises(Exception) as ex: 55 | pglogin = LoginPage(wp) 56 | pglogin.timeout = 2 57 | pglogin.unknownElement 58 | assert 'unknownElement - locator: (css selector, somethingWrong)' in str(ex.value) 59 | --------------------------------------------------------------------------------