{ "info": { "author": "valerio morsella", "author_email": "valerio.morsella@skyscanner.net", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 2.7", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Testing" ], "description": "pages\n=====\n\n|BuildStatus| |CoverageStatus| |PyPI1| |PyPI2| |PyPI3|\n\n*pages* is a lightweight Python library which helps in the creation of\nreadable and reliable page/component objects for UI tests.\n\nIt is been designed to ensure that timing issues will have zero impact\non your test results.\n\nIt is a wrapper around the Python WebDriver bindings, but the same ideas\n(components, traits, ...) could be adapted to any other driver technology - \nincluding mobile.\n\nIntroduction\n============\n\nThe most common problem when introducing automated UI-based testing in\ncontinuous integration is the brittle nature of the tests. A false negative in a CI\npipeline is often cause of stress, fierce discussions (slip the build\nthrough vs. hold it on failure analysis) and in some cases radical\nchanges of test strategies. However, the value of reliable UI tests is\nundeniable, as they are the closest thing to real usage of a product.\nMoreover, they exercise the stack from the front-end, thus representing a way\nto test integration of the whole system. This is why automated UI tests\nsit at the top of the well-known test pyramid: they are seen as\ndifficult to implement and expensive to maintain.\n\nHowever, reliability of tests is normally a *design problem*.\n\n*pages* offers a simple but effective framework to build robust page\nobjects for UI tests.\n\nInstallation\n============\n\n.. code:: bash\n\n pip install p-ages\n\nDesign\n======\n\nThe design revolves around three key concepts:\n\n- the `Page `_ class\n- page `traits `_\n- the `UIComponent `_ class\n\nAs usual, the best way to learn how to use it is to start coding.\n\nExample\n-------\n\nWe want to create UI tests for this page:\nhttp://the-internet.herokuapp.com/login.\n\nThis is a login page that, on successful authentication, takes to a\nsecure area page. We want to write a test that loads the login page and\nexecutes authentication. We will create two page objects. All the\nexamples are in the\n`samples `__\nfolder.\n\nFirst step - test container\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs a first step, we will create a container where we instantiate the\ndriver.\n\n.. code:: python\n\n class LoginTest(unittest.TestCase):\n\n def setUp(self):\n self.driver = WebDriver()\n\n def test_can_login(self):\n pass\n\nSecond step - test implementation top-down\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nLoad the page, enter credentials and assert that the secure area page is\nloaded. In code this becomes:\n\n.. code:: python\n\n class LoginTest(unittest.TestCase):\n\n def setUp(self):\n self.driver = WebDriver()\n\n def test_can_login(self):\n login_page = LoginPage(self.driver).load().wait_until_loaded()\n\n secure_area_page = login_page.login('tomsmith', 'SuperSecretPassword!')\n\n assert_that(secure_area_page, is_loaded().with_timeout(PAGE_LOADING_TIMEOUT)\n .with_polling(POLLING_INTERVAL))\n\nNotice how the LoginPage needs only a reference to the driver that we have\ncreated in the setUp. We know the API already, so we are adding method\ncalls to load() and wait\\_until\\_loaded(). However, this will be\nexplained in the next steps.\n\nThird step - loading the Login page\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe Login page will extend the Page base class from our framework. One\nrequirement is that load(), which is abstract, has to be defined.\nMoreover, since we are chaining other methods, load() has to return an\ninstance of the class.\n\n.. code:: python\n\n class LoginPage(Page):\n\n def __init__(self, driver):\n Page.__init__(driver, 'Login page')\n\n def load(self):\n self.driver.get(LOGIN_PAGE_URL)\n return self\n\nFourth step - adding traits\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n*Traits* are the conditions that have to be verified for the page to be\nin the loaded state. In our case, the page has user text input, password\ntext input and submit button, since those are the elements we are going to interact with.\nWe'll start by defining three private methods to check the presence of\nthose elements.\n\n.. code:: python\n\n def _has_username_input(self):\n return TextInput(self.driver, 'username', [By.ID, 'username']).is_present()\n\n def _has_password_input(self):\n return TextInput(self.driver, 'password', [By.ID, 'password']).is_present()\n\n def _has_submit_button(self):\n return Button(self.driver, 'submit', [By.XPATH, \"//button[@type = 'submit']\"]).is_present()\n\nWe can now add *traits* to the page under test. Let's add them to\nthe \\_\\_init\\_\\_().\n\n.. code:: python\n\n def __init__(self, driver):\n Page.__init__(self, driver, 'Login page')\n self.add_trait(self._has_username_input, 'has username')\n self.add_trait(self._has_password_input, 'has password')\n self.add_trait(self._has_submit_button, 'has submit button')\n\nNotice how add\\_trait() takes as first parameter the method name. In\nother words, it accepts only a callable. For instance, you may pass\na lambda to it. The second parameter is the short description of the\ntrait, used for logging.\n\nFinally, notice how the three traits we chose are the elements that\nneed to be ready for the interactions we are going to have with the\npage. While these three traits are verified, other parts of the page may\nstill be loading. While this should not be a problem for the safety of this test,\nin general great care should be taken to select traits so that tests do not interact\nwith parts of the DOM which have not finished loading.\n\nFifth step - logging in and returning secure area page\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nOn successful login, the secure area page should be returned. This is done\nin the login\\_user() method. Notice that we have refactored some of the\nprevious code for better reuse.\n\n.. code:: python\n\n def login_user(self, username, password):\n self._user_name().input_text(username)\n self._password().input_text(password)\n self._submit_button().click()\n return SecureAreaPage(self.driver)\n\nSixth step - Secure Area Page\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nFinally, we need to implement the return page. Similarly to the login\npage:\n\n.. code:: python\n\n class SecureAreaPage(Page):\n\n def __init__(self, driver):\n Page.__init__(self, driver, 'Secure area page')\n self.add_trait(self._has_logout_button, 'has logout button')\n\n def load(self):\n raise NotLoadablePageException('{0} cannot be loaded'.format(self.name))\n\n def _has_logout_button(self):\n return Button(self.driver, [By.XPATH, \"//button[@href='/logout']\"]).is_present()\n\nNotice how we did not implement load(), since the secure area page is not\nloadable from a URL.\n\nPage objects\n------------\n\nIn the previous example, we have seen how simple it is to implement page\nobjects and create tests with them. In essence, all we need to do is:\n\n* extend the Page class\n* implement the load() method\n* add traits to the page\n\nAs a final (golden) rule, every method which models a user interaction\nand results in a page load has to return a page object of the target page.\nThe simplest case is load() itself.\n\nThe benefit of building a page from the Page class is that, after proper\ndefinition of traits, we can rely on wait\\_until\\_loaded() to\npause the test execution *just enough* to allow the page to load.\n\n.. code:: python\n\n login_page = LoginPage(self.driver).load().wait_until_loaded()\n\nPage traits\n-----------\n\nDisclaimer: Traits we define here are not \"class traits\".\n\n*A Trait is an abstraction of the condition that must be verified for an\nelement to be ready.* As shown in the example above, adding traits is\nextremely simple. The most important reason we introduced traits is\nthat they make it easy to nail down which conditions have\nfailed on page load.\n\nUIComponents\n------------\n\nThe UIComponent class is the basic element we use to build our page models.\nAnything that is part of a web page can be modelled as a UIComponent.\nThe responsibility of this class is to ensure lazy creation of a\nWebElement.\n\nIn the example above, the InputText and Button classes extend UIComponent.\n\nIn general, a UIComponent may represent any portion of the DOM. It is\nimportant to notice that a UIComponent can contain another UIComponent. An\nexample of this is the Table class.\n\nExample\n~~~~~~~\n\nWe want to build a model of the table at this address:\nhttp://the-internet.herokuapp.com/challenging\\_dom. We will build a\ncomponent class that allows interaction with the table. In particular,\nwe want to test that elements in the first row of the table match the\nexpected values. The complete example code can be found under the\n`sample `__\nfolder.\n\nAgain, we will build the test top-down.\n\n.. code:: python\n\n EXPECTED_LABEL_LIST = ['Iuvaret0', 'Apeirian0', 'Adipisci0', 'Definiebas0', 'Consequuntur0', 'Phaedrum0', 'edit delete']\n\n class SampleTableTest(unittest.TestCase):\n\n def setUp(self):\n self.driver = WebDriver()\n\n def tearDown(self):\n self.driver.quit()\n\n def test_can_get_table_elements(self):\n sample_page = SamplePage(self.driver).load().wait_until_loaded()\n first_table_row_values = sample_page.read_first_table_row()\n\n assert_that(first_table_row_values, equal_to(EXPECTED_LABEL_LIST))\n\nSamplePage is a page object class which contains a table as a component.\nWe can start by writing the table. The Table class (available in\npages.standard\\_components) makes this simple.\n\n.. code:: python\n\n class SampleTable(Table):\n\n def __init__(self, driver):\n super(SampleTable, self).__init__(driver, 'sample table', [By.XPATH, './tbody/tr'], TableRow, 'row',\n [By.XPATH, '//table'])\n\nSampleTable extends Table which in turn extends UIComponent.\nMoreover, when calling the super() method, we define TableRow as a\ncomponent representing a single row.\n\n.. code:: python\n\n class TableRow(UIComponent):\n\n def __init__(self, driver, name):\n super(TableRow, self).__init__(driver, name)\n\n def values(self):\n return [i.text for i in self.locate().find_elements_by_xpath('./td')]\n\nTableRow extends UIComponent and defines methods to access elements\nin the row. The main problem has been split into smaller ones, and\nwe have written a very small amount of code.\n\nFinally, we can define SamplePage.\n\n.. code:: python\n\n class SamplePage(Page):\n\n def __init__(self, driver):\n Page.__init__(self, driver, 'sample page')\n self.add_trait(lambda: SampleTable(self.driver).is_present(), 'has table')\n\n def load(self):\n self.driver.get('http://the-internet.herokuapp.com/challenging_dom')\n return self\n\n def read_first_table_row(self):\n table_rows = SampleTable(self.driver).get_items()\n return [i for i in table_rows[0].values()]\n\nOne thing to notice here is that the table object is created afresh\nevery time read\\_first\\_table\\_row() is called. While this makes sense\nin most cases, as the content of the page may change dynamically after\nloading (this is often the case for tables), in this case inspection of\nthe Table class tells us that calling \\_\\_init\\_\\_() does not result in\nany WebDriver operation. The only moment when we locate elements on the\nDOM is when we call get\\_items().\n\nThis is the other key-concept of *pages*: by using UIComponent, we can\nbuild components that instantiate a WebElement only when we need to use\nit. This eliminates the possibility of StaleElementReferenceException(s)\nto be raised during the execution.\n\nLogging\n=======\n\n*pages* adds only a NullHandler to the loggers.\nIn order to turn on logging generated inside the library you can rely on root logger or set on explicitly.\nTo turn on logging from your application code, for instance:\n\n.. code:: python\n\n logging.getLogger('pages').setLevel(logging.DEBUG)\n logging.basicConfig(level=logging.INFO)\n\n\nThis will set log level to DEBUG.\n\n\nDistributing pages\n==================\n\n*pages* is distributed on PyPI.\n\nInstructions\n------------\n\n- Ensure .pypirc is present.\n- Update \\_\\_version\\_\\_ under pages/\\_\\_init\\_\\_.py.\n- Run *distribute.sh* under the *script* folder.\n\nLicense\n=======\n\n*pages* is licensed under the Apache Software License 2.0 provision.\n\nAuthor\n=======\n\n`Valerio Morsella `_\n\n\n.. |BuildStatus| image:: https://travis-ci.org/Skyscanner/pages.svg\n :target: https://travis-ci.org/Skyscanner/pages\n.. |CoverageStatus| image:: https://coveralls.io/repos/Skyscanner/pages/badge.svg?branch=master&service=github\n :target: https://coveralls.io/github/Skyscanner/pages?branch=master\n.. |PyPI1| image:: https://img.shields.io/pypi/v/p-ages.svg\n :target: https://pypi.python.org/pypi/p-ages\n.. |PyPI2| image:: https://img.shields.io/pypi/wheel/p-ages.svg\n :target: https://img.shields.io/pypi/wheel/p-ages.svg\n.. |PyPI3| image:: https://img.shields.io/pypi/dm/p-ages.svg\n :target: https://pypi.python.org/pypi/p-ages", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/Skyscanner/pages", "keywords": null, "license": "ASL v.2.0", "maintainer": null, "maintainer_email": null, "name": "p-ages", "package_url": "https://pypi.org/project/p-ages/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/p-ages/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/Skyscanner/pages" }, "release_url": "https://pypi.org/project/p-ages/1.0.1/", "requires_dist": null, "requires_python": null, "summary": "pages is a lightweight page object and component Python library for UI tests", "version": "1.0.1" }, "last_serial": 1947017, "releases": { "0.1.0": [], "0.1.1": [ { "comment_text": "", "digests": { "md5": "143d480cb007cff4f0093d133d897cad", "sha256": "c8458db599fd251497af856788411904c5222f229f8a4c97fd6153347c0b9300" }, "downloads": -1, "filename": "p_ages-0.1.1-py2-none-any.whl", "has_sig": false, "md5_digest": "143d480cb007cff4f0093d133d897cad", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29306, "upload_time": "2015-12-04T11:22:51", "url": "https://files.pythonhosted.org/packages/85/18/c0da18863579927ebafc84db9bae46976418b56adb590b65441c16964b98/p_ages-0.1.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "2bda574109ec9bff52e29a6e6f5e3cfc", "sha256": "ca91b859d24393fe5822455ace096a2aa666a1120091006ff0db4b3114ba42ab" }, "downloads": -1, "filename": "p-ages-0.1.1.tar.gz", "has_sig": false, "md5_digest": "2bda574109ec9bff52e29a6e6f5e3cfc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22708, "upload_time": "2015-12-04T11:22:44", "url": "https://files.pythonhosted.org/packages/1d/1d/813f49e3ecab7c474d46604238544731bc9f42541a831cf243bd65093d64/p-ages-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "9ad74f74c1f2521a4cecf006c5f21a64", "sha256": "9d7836852627fb33878215d2f40eb2149872d3b35e98ad8a7681c490ae2f2e87" }, "downloads": -1, "filename": "p_ages-0.1.2-py2-none-any.whl", "has_sig": false, "md5_digest": "9ad74f74c1f2521a4cecf006c5f21a64", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29759, "upload_time": "2016-01-13T14:02:27", "url": "https://files.pythonhosted.org/packages/8c/6d/13a31bde78bc60fcf1850f9e8eb91f2ca15ab53c88f0f22fe4997fafe65e/p_ages-0.1.2-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "61458d735cb942e52278a8e8b198e716", "sha256": "3fe2b211e72ebb55ece21b6d8a70165d23f8ac7be1d1dde46213037884576e67" }, "downloads": -1, "filename": "p-ages-0.1.2.tar.gz", "has_sig": false, "md5_digest": "61458d735cb942e52278a8e8b198e716", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23275, "upload_time": "2016-01-13T14:01:50", "url": "https://files.pythonhosted.org/packages/19/67/cbb23c8477b9eae878a808062f1915ce24998dffe9118b93f854b4134325/p-ages-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "5715cd938217ac8afc1e50c6de402261", "sha256": "b69f2a51a47dfeb47fa396eda6d07af7a063d0380ac8d575f41412f469f12a3a" }, "downloads": -1, "filename": "p_ages-0.1.3-py2-none-any.whl", "has_sig": false, "md5_digest": "5715cd938217ac8afc1e50c6de402261", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29761, "upload_time": "2016-01-13T14:29:52", "url": "https://files.pythonhosted.org/packages/5b/89/110c2d5ff42ded981035e5413cb6d1745c616b2b25f9da0c75077087c3ad/p_ages-0.1.3-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ae86a83f54907297eb64bc81a1c3ccc6", "sha256": "b458b6a861307bbd5051dcc23a3b248374e6ba678da4c0130fcbdd7037df0e97" }, "downloads": -1, "filename": "p-ages-0.1.3.tar.gz", "has_sig": false, "md5_digest": "ae86a83f54907297eb64bc81a1c3ccc6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23269, "upload_time": "2016-01-13T14:29:46", "url": "https://files.pythonhosted.org/packages/c9/6a/14d03f0e7f6f8567f8e0d85061b4afd9566644c6cafbd3881acd896adefb/p-ages-0.1.3.tar.gz" } ], "1.0.0": [ { "comment_text": "", "digests": { "md5": "6eac28015f29c2e9643da98f22866b64", "sha256": "30c1bedc951fc914209b1cd162695287f09b4fa451ed296e3ed06778ef869b0b" }, "downloads": -1, "filename": "p_ages-1.0.0-py2-none-any.whl", "has_sig": false, "md5_digest": "6eac28015f29c2e9643da98f22866b64", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29820, "upload_time": "2016-02-09T08:42:31", "url": "https://files.pythonhosted.org/packages/40/94/3bc9ba7b539ea0f0674d6c86babecf1275dfc7db7201dcde41a6c91e5357/p_ages-1.0.0-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "e914f1e318aa4d9805454802f5714233", "sha256": "d455f63b63af7cc1ad3bb0d910af6a8597693d7aa7efdad29449bd4bae6926ec" }, "downloads": -1, "filename": "p-ages-1.0.0.tar.gz", "has_sig": false, "md5_digest": "e914f1e318aa4d9805454802f5714233", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23332, "upload_time": "2016-02-09T08:42:19", "url": "https://files.pythonhosted.org/packages/1f/5d/8967eabdd55e46dc21114a1d814d73bcbbfa322d75811ee4e7b5b3a11417/p-ages-1.0.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "24915380f894ecc61ce8515a24eb49df", "sha256": "17eedef43955cc8de1c0ea8f869df6de8913ee1dd7883ad322f9acb30a0a2c9b" }, "downloads": -1, "filename": "p_ages-1.0.1-py2-none-any.whl", "has_sig": false, "md5_digest": "24915380f894ecc61ce8515a24eb49df", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29825, "upload_time": "2016-02-09T08:51:34", "url": "https://files.pythonhosted.org/packages/fc/db/4b0ae7a3717b30dce965267c41c212411114ef0cbeda83e726438f2d5c36/p_ages-1.0.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6f1ab350aec45fd3631e7f30c0e7bf3b", "sha256": "e820041f5659a0487ab1e2a91d3517f79fc042640e17335470a4a6d494f6e5ef" }, "downloads": -1, "filename": "p-ages-1.0.1.tar.gz", "has_sig": false, "md5_digest": "6f1ab350aec45fd3631e7f30c0e7bf3b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23342, "upload_time": "2016-02-09T08:51:28", "url": "https://files.pythonhosted.org/packages/df/db/3adc4a8420b20b91e0eb56f3ece173901a24accfa8c7b4ca006312cd833e/p-ages-1.0.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "24915380f894ecc61ce8515a24eb49df", "sha256": "17eedef43955cc8de1c0ea8f869df6de8913ee1dd7883ad322f9acb30a0a2c9b" }, "downloads": -1, "filename": "p_ages-1.0.1-py2-none-any.whl", "has_sig": false, "md5_digest": "24915380f894ecc61ce8515a24eb49df", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 29825, "upload_time": "2016-02-09T08:51:34", "url": "https://files.pythonhosted.org/packages/fc/db/4b0ae7a3717b30dce965267c41c212411114ef0cbeda83e726438f2d5c36/p_ages-1.0.1-py2-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6f1ab350aec45fd3631e7f30c0e7bf3b", "sha256": "e820041f5659a0487ab1e2a91d3517f79fc042640e17335470a4a6d494f6e5ef" }, "downloads": -1, "filename": "p-ages-1.0.1.tar.gz", "has_sig": false, "md5_digest": "6f1ab350aec45fd3631e7f30c0e7bf3b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 23342, "upload_time": "2016-02-09T08:51:28", "url": "https://files.pythonhosted.org/packages/df/db/3adc4a8420b20b91e0eb56f3ece173901a24accfa8c7b4ca006312cd833e/p-ages-1.0.1.tar.gz" } ] }