citronella


Namecitronella JSON
Version 1.0.1 PyPI version JSON
download
home_pagehttps://github.com/heyclore/citronella/tree/main/python#readme
SummaryWebdriver Extension with Page Object Wrapper
upload_time2023-06-29 04:07:31
maintainer
docs_urlNone
authorheyclore
requires_python~=3.7
licenseMIT
keywords test unittest pytest webdriver appium selenium
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Citronella

[![PyPI version](https://badge.fury.io/py/citronella.svg)](https://badge.fury.io/py/citronella)
[![Downloads](https://pepy.tech/badge/citronella)](https://pepy.tech/project/citronella)

webdriver extension with a page object wrapper.

![alt terminal](https://github.com/heyclore/citronella/blob/main/python/screenshot/terminal.png?raw=true)
![alt pytest-html](https://github.com/heyclore/citronella/blob/main/python/screenshot/pytest_html.png?raw=true)
![alt github-action](https://github.com/heyclore/citronella/blob/main/python/screenshot/github_action.png?raw=true)

## Example Tests

### Selenium

```python
import pytest
from Pages.contents_page import ContentsPage


class TestNavigationMenu:

    def test_help_page(self, web):
        web.driver.get('https://pypi.org/')
        web.page = ContentsPage

        web.page.home_page.help_button.click()
        assert 'Help' in web.driver.title

    def test_sponsors_page(self, web):
        web.page.help_page.sponsors_button.click()
        assert 'Sponsors' in web.driver.title

    def test_login_page(self, web):
        web.page.sponsors_page.login_button.click()
        assert 'Log' in web.driver.title

    def test_register_page(self, web):
        web.page.login_page.register_button.click()
        assert 'Create' in web.driver.title
```

### Appium

```python
import pytest
from Pages.contents_page import ContentsPage


class TestInput:

    def test_input(self, web):
        web.page = ContentsPage
        web.page.home_page.gallery_button.click()
        web.page.gallery_page.text_input.send_keys('citronella')
        web.page.gallery_page.add_button.click()
        elements = web.page.gallery_page.text_lists.get_elements()
        assert 'citronella' in [x.text for x in elements]
```

Even though this module is mainly designed for the page object model, it can also be used without it for quick prototyping or mockups, etc.
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from citronella import WebPage


driver = webdriver.Chrome()

web = WebPage(driver, webdriver_wait=20, logger=False)
web.driver.get('https://pypi.org/')

web.locate(By.ID, 'search').get_element().send_keys('citronella')
web.locate(By.XPATH, '//button[@type="submit"]/i').get_element().click()

elements = web.locate(By.XPATH, '//span[@class="package-snippet__name"]')
if elements.ec_visibility_of_all_elements_located():
    results = elements.get_elements()
    text_lists = [x.text for x in results]
```

___
## Install Package

```bash
pip install citronella
```

___
## Documentation

There are only two modules import in this package:

* The first module is for conftest.py.

### Selenium

```python
import pytest
from selenium import webdriver
from citronella import WebPage


@pytest.fixture(autouse=True, scope='class')
def web(request):
    driver = webdriver.Chrome()
    yield WebPage(driver)
    driver.quit()
```

### Appium

```python
import pytest
import os
from appium import webdriver
from appium.options.android import UiAutomator2Options
from citronella import WebPage


@pytest.fixture(autouse='true', scope='class')
def web(request):
    options = UiAutomator2Options()
    options.platformName = 'Android'
    options.app = os.getcwd() + '/APK/ApiDemos-debug.apk.zip'
    driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', options=options)
    yield WebPage(driver)
    driver.quit()
```

* The second module are for the page object model.

### Selenium

```python
from selenium.webdriver.common.by import By
from citronella import ui
from Pages.components import Header


class HomePage(Header):

    def some_button(self):
        return ui(By.XPATH, '//a[@name="foo"]')

    def search_input(self):
        return ui(By.ID, 'search')

    def search_button(self):
        return ui(By.NAME, 'search-button')
```

### Appium

```python
from appium.webdriver.common.appiumby import AppiumBy
from citronella import ui
from Pages.components import Header


class HomePage(Header):

    def some_button(self):
        return ui(AppiumBy.XPATH, '//a[@name="foo"]')

    def search_input(self):
        return ui(AppiumBy.ACCESSIBILITY_ID, 'search')

    def search_button(self):
        return ui(AppiumBy.ID, 'search-button')
```

___
## Page Object Design / Strategy

There's a two ways to create a page object for `WebPage`:

1. Straightforward approach: This method requires importing the page object for each test.
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from citronella import WebPage, ui


class HomePage:
    def auth_buton(self):
        return ui(By.XPATH, '//a[@name="foo"]')

class LoginPage:
    def email_input(self):
        return ui(By.ID, 'email')

    def password_input(self):
        return ui(By.ID, 'password')

    def login_buton(self):
        return ui(By.ID, 'login')

driver = webdriver.Chrome()
web = WebPage(driver)
web.driver.get('https://foobarbaz.com/')
web.page = HomePage
web.page.auth_button.click()
web.page = LoginPage
web.page.email_input.send_keys('foo')
web.page.password_input.send_keys('bar')
web.page.login_button.click()
```

2. Lazy loading approach: This method is slightly more complex but offers the benefit of lazy loading.
see [ContentsPage example](https://github.com/heyclore/citronella/blob/main/python/example/selenium/Pages/contents_page.py)
or this [Page object example](https://github.com/heyclore/citronella/tree/main/python/example/selenium/Pages)
```python
from selenium import webdriver
from selenium.webdriver.common.by import By
from citronella import WebPage, ui


class ContentsPage:
    def home_page(self):
        return HomePage

    def login_page(self):
        return LoginPage

class HomePage:
    def auth_buton(self):
        return ui(By.XPATH, '//a[@name="foo"]')

class LoginPage:
    def email_input(self):
        return ui(By.ID, 'email')

    def password_input(self):
        return ui(By.ID, 'password')

    def login_buton(self):
        return ui(By.ID, 'login')

driver = webdriver.Chrome()
web = WebPage(driver)
web.driver.get('https://foobarbaz.com/')
web.page = ContentsPage
web.page.home_page.auth_button.click()
web.page.login_page.email_input.send_keys('foo')
web.page.login_page.password_input.send_keys('bar')
web.page.login_page.login_button.click()
```

___
## Usage

### citronella.WebPage

###### Args:
- driver / webdriver

###### Kwargs (optional):
- webdriver_wait `number(seconds)`, default value is `10`
- logger `bool`, default value is `True`

###### Method Lists:
| Method Name        | Args*       | Kwargs**         | Note |
| ------------------ |:-----------:|:----------------:|:----:|
| driver             | -           | -                | return selenium `webdriver` object |
| locate             | by, value   | -                | similar as`driver.get_element` as input & return [citronella.WebUi](https://github.com/heyclore/citronella/tree/main/python#citronellaui--citronellawebui) object |
| page               | page object | -                | setter |
| page               | -           | -                | getter |
| webdriver_wait     | number(sec) | -                |      |
| ready_state        | number(sec) | -                | execute javascript `document.readyState` manually, default timeout is `30` |
| sleep              | number(sec) | -                |      |

### citronella.ui / citronella.WebUi

###### Args:
- by
- value

###### Method Lists:
| Method Name   | Args*  | Kwargs**           | Note |
| ------------- |:------:|:------------------:|:----:|
| send_keys     | text   | clear `bool`, return_key `bool` |     |
| click         | -      | -                  |      |
| get_element   | -      | -                  |      |
| get_elements  | -      | -                  |      |
| ec_element_to_be_clickable | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_presence_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_presence_of_all_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_visibility_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_visibility_of_all_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_visibility_of_any_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_invisibility_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |
| ec_element_located_to_be_selected | -    | -    | wrapper of `EC` / `excpected_condition` |


## Testing powered by
<a target="_blank" href="https://www.browserstack.com/"><img width="200" src="https://www.browserstack.com/images/layout/browserstack-logo-600x315.png"></a><br>
[BrowserStack Open-Source Program](https://www.browserstack.com/open-source)

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/heyclore/citronella/tree/main/python#readme",
    "name": "citronella",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "~=3.7",
    "maintainer_email": "",
    "keywords": "test,unittest,pytest,webdriver,appium,selenium",
    "author": "heyclore",
    "author_email": "cloore@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/b2/40/3808891e6374218157dbb28232177be085c4648dc333a5cf43d22d89f92d/citronella-1.0.1.tar.gz",
    "platform": null,
    "description": "# Citronella\n\n[![PyPI version](https://badge.fury.io/py/citronella.svg)](https://badge.fury.io/py/citronella)\n[![Downloads](https://pepy.tech/badge/citronella)](https://pepy.tech/project/citronella)\n\nwebdriver extension with a page object wrapper.\n\n![alt terminal](https://github.com/heyclore/citronella/blob/main/python/screenshot/terminal.png?raw=true)\n![alt pytest-html](https://github.com/heyclore/citronella/blob/main/python/screenshot/pytest_html.png?raw=true)\n![alt github-action](https://github.com/heyclore/citronella/blob/main/python/screenshot/github_action.png?raw=true)\n\n## Example Tests\n\n### Selenium\n\n```python\nimport pytest\nfrom Pages.contents_page import ContentsPage\n\n\nclass TestNavigationMenu:\n\n    def test_help_page(self, web):\n        web.driver.get('https://pypi.org/')\n        web.page = ContentsPage\n\n        web.page.home_page.help_button.click()\n        assert 'Help' in web.driver.title\n\n    def test_sponsors_page(self, web):\n        web.page.help_page.sponsors_button.click()\n        assert 'Sponsors' in web.driver.title\n\n    def test_login_page(self, web):\n        web.page.sponsors_page.login_button.click()\n        assert 'Log' in web.driver.title\n\n    def test_register_page(self, web):\n        web.page.login_page.register_button.click()\n        assert 'Create' in web.driver.title\n```\n\n### Appium\n\n```python\nimport pytest\nfrom Pages.contents_page import ContentsPage\n\n\nclass TestInput:\n\n    def test_input(self, web):\n        web.page = ContentsPage\n        web.page.home_page.gallery_button.click()\n        web.page.gallery_page.text_input.send_keys('citronella')\n        web.page.gallery_page.add_button.click()\n        elements = web.page.gallery_page.text_lists.get_elements()\n        assert 'citronella' in [x.text for x in elements]\n```\n\nEven though this module is mainly designed for the page object model, it can also be used without it for quick prototyping or mockups, etc.\n```python\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom citronella import WebPage\n\n\ndriver = webdriver.Chrome()\n\nweb = WebPage(driver, webdriver_wait=20, logger=False)\nweb.driver.get('https://pypi.org/')\n\nweb.locate(By.ID, 'search').get_element().send_keys('citronella')\nweb.locate(By.XPATH, '//button[@type=\"submit\"]/i').get_element().click()\n\nelements = web.locate(By.XPATH, '//span[@class=\"package-snippet__name\"]')\nif elements.ec_visibility_of_all_elements_located():\n    results = elements.get_elements()\n    text_lists = [x.text for x in results]\n```\n\n___\n## Install Package\n\n```bash\npip install citronella\n```\n\n___\n## Documentation\n\nThere are only two modules import in this package:\n\n* The first module is for conftest.py.\n\n### Selenium\n\n```python\nimport pytest\nfrom selenium import webdriver\nfrom citronella import WebPage\n\n\n@pytest.fixture(autouse=True, scope='class')\ndef web(request):\n    driver = webdriver.Chrome()\n    yield WebPage(driver)\n    driver.quit()\n```\n\n### Appium\n\n```python\nimport pytest\nimport os\nfrom appium import webdriver\nfrom appium.options.android import UiAutomator2Options\nfrom citronella import WebPage\n\n\n@pytest.fixture(autouse='true', scope='class')\ndef web(request):\n    options = UiAutomator2Options()\n    options.platformName = 'Android'\n    options.app = os.getcwd() + '/APK/ApiDemos-debug.apk.zip'\n    driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', options=options)\n    yield WebPage(driver)\n    driver.quit()\n```\n\n* The second module are for the page object model.\n\n### Selenium\n\n```python\nfrom selenium.webdriver.common.by import By\nfrom citronella import ui\nfrom Pages.components import Header\n\n\nclass HomePage(Header):\n\n    def some_button(self):\n        return ui(By.XPATH, '//a[@name=\"foo\"]')\n\n    def search_input(self):\n        return ui(By.ID, 'search')\n\n    def search_button(self):\n        return ui(By.NAME, 'search-button')\n```\n\n### Appium\n\n```python\nfrom appium.webdriver.common.appiumby import AppiumBy\nfrom citronella import ui\nfrom Pages.components import Header\n\n\nclass HomePage(Header):\n\n    def some_button(self):\n        return ui(AppiumBy.XPATH, '//a[@name=\"foo\"]')\n\n    def search_input(self):\n        return ui(AppiumBy.ACCESSIBILITY_ID, 'search')\n\n    def search_button(self):\n        return ui(AppiumBy.ID, 'search-button')\n```\n\n___\n## Page Object Design / Strategy\n\nThere's a two ways to create a page object for `WebPage`:\n\n1. Straightforward approach: This method requires importing the page object for each test.\n```python\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom citronella import WebPage, ui\n\n\nclass HomePage:\n    def auth_buton(self):\n        return ui(By.XPATH, '//a[@name=\"foo\"]')\n\nclass LoginPage:\n    def email_input(self):\n        return ui(By.ID, 'email')\n\n    def password_input(self):\n        return ui(By.ID, 'password')\n\n    def login_buton(self):\n        return ui(By.ID, 'login')\n\ndriver = webdriver.Chrome()\nweb = WebPage(driver)\nweb.driver.get('https://foobarbaz.com/')\nweb.page = HomePage\nweb.page.auth_button.click()\nweb.page = LoginPage\nweb.page.email_input.send_keys('foo')\nweb.page.password_input.send_keys('bar')\nweb.page.login_button.click()\n```\n\n2. Lazy loading approach: This method is slightly more complex but offers the benefit of lazy loading.\nsee [ContentsPage example](https://github.com/heyclore/citronella/blob/main/python/example/selenium/Pages/contents_page.py)\nor this [Page object example](https://github.com/heyclore/citronella/tree/main/python/example/selenium/Pages)\n```python\nfrom selenium import webdriver\nfrom selenium.webdriver.common.by import By\nfrom citronella import WebPage, ui\n\n\nclass ContentsPage:\n    def home_page(self):\n        return HomePage\n\n    def login_page(self):\n        return LoginPage\n\nclass HomePage:\n    def auth_buton(self):\n        return ui(By.XPATH, '//a[@name=\"foo\"]')\n\nclass LoginPage:\n    def email_input(self):\n        return ui(By.ID, 'email')\n\n    def password_input(self):\n        return ui(By.ID, 'password')\n\n    def login_buton(self):\n        return ui(By.ID, 'login')\n\ndriver = webdriver.Chrome()\nweb = WebPage(driver)\nweb.driver.get('https://foobarbaz.com/')\nweb.page = ContentsPage\nweb.page.home_page.auth_button.click()\nweb.page.login_page.email_input.send_keys('foo')\nweb.page.login_page.password_input.send_keys('bar')\nweb.page.login_page.login_button.click()\n```\n\n___\n## Usage\n\n### citronella.WebPage\n\n###### Args:\n- driver / webdriver\n\n###### Kwargs (optional):\n- webdriver_wait `number(seconds)`, default value is `10`\n- logger `bool`, default value is `True`\n\n###### Method Lists:\n| Method Name        | Args*       | Kwargs**         | Note |\n| ------------------ |:-----------:|:----------------:|:----:|\n| driver             | -           | -                | return selenium `webdriver` object |\n| locate             | by, value   | -                | similar as`driver.get_element` as input & return [citronella.WebUi](https://github.com/heyclore/citronella/tree/main/python#citronellaui--citronellawebui) object |\n| page               | page object | -                | setter |\n| page               | -           | -                | getter |\n| webdriver_wait     | number(sec) | -                |      |\n| ready_state        | number(sec) | -                | execute javascript `document.readyState` manually, default timeout is `30` |\n| sleep              | number(sec) | -                |      |\n\n### citronella.ui / citronella.WebUi\n\n###### Args:\n- by\n- value\n\n###### Method Lists:\n| Method Name   | Args*  | Kwargs**           | Note |\n| ------------- |:------:|:------------------:|:----:|\n| send_keys     | text   | clear `bool`, return_key `bool` |     |\n| click         | -      | -                  |      |\n| get_element   | -      | -                  |      |\n| get_elements  | -      | -                  |      |\n| ec_element_to_be_clickable | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_presence_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_presence_of_all_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_visibility_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_visibility_of_all_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_visibility_of_any_elements_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_invisibility_of_element_located | -    | -    | wrapper of `EC` / `excpected_condition` |\n| ec_element_located_to_be_selected | -    | -    | wrapper of `EC` / `excpected_condition` |\n\n\n## Testing powered by\n<a target=\"_blank\" href=\"https://www.browserstack.com/\"><img width=\"200\" src=\"https://www.browserstack.com/images/layout/browserstack-logo-600x315.png\"></a><br>\n[BrowserStack Open-Source Program](https://www.browserstack.com/open-source)\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Webdriver Extension with Page Object Wrapper",
    "version": "1.0.1",
    "project_urls": {
        "Homepage": "https://github.com/heyclore/citronella/tree/main/python#readme",
        "Source": "https://github.com/heyclore/citronella/tree/main/python"
    },
    "split_keywords": [
        "test",
        "unittest",
        "pytest",
        "webdriver",
        "appium",
        "selenium"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "138e6d546822917c5cd349482a15301ac4b5d059cfa620c6783c8bb9e5a7fafc",
                "md5": "2c1f4db0ce26319cb6cf3cfe259313a1",
                "sha256": "04ec3fe4355875fb24ac0cd0b4751c50aaad6d3e7f2169b595bee06ede99820a"
            },
            "downloads": -1,
            "filename": "citronella-1.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "2c1f4db0ce26319cb6cf3cfe259313a1",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "~=3.7",
            "size": 12874,
            "upload_time": "2023-06-29T04:07:28",
            "upload_time_iso_8601": "2023-06-29T04:07:28.925041Z",
            "url": "https://files.pythonhosted.org/packages/13/8e/6d546822917c5cd349482a15301ac4b5d059cfa620c6783c8bb9e5a7fafc/citronella-1.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "b2403808891e6374218157dbb28232177be085c4648dc333a5cf43d22d89f92d",
                "md5": "1a992e7b1a961cd31f533bc71f86f041",
                "sha256": "6a0abdd1db01a2c683802d2f2857a7f22a219b92870e5fa26280da854301e209"
            },
            "downloads": -1,
            "filename": "citronella-1.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "1a992e7b1a961cd31f533bc71f86f041",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "~=3.7",
            "size": 9900,
            "upload_time": "2023-06-29T04:07:31",
            "upload_time_iso_8601": "2023-06-29T04:07:31.179971Z",
            "url": "https://files.pythonhosted.org/packages/b2/40/3808891e6374218157dbb28232177be085c4648dc333a5cf43d22d89f92d/citronella-1.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-06-29 04:07:31",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "heyclore",
    "github_project": "citronella",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "citronella"
}
        
Elapsed time: 0.11231s