==============
Robot Tutorial
==============

This tutorial will step you through writing your first test, then
enhancing that test with a custom keyword implemented as a page
object. It is not a comprehensive tutorial on using Robot
Framework. For Robot Framework documentation see the
`Robot Framework User Guide <http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html>`_

It is assumed you've worked through the CumulusCI :doc:`tutorial` at least up to the
point where you've called ``cci project init``. It is also assumed
that you've read the :doc:`robotframework` section of this document, which gives
an overview of CumulusCI / Robot Framework integration.



Part 1: Folder Structure
========================

We recommend that all robot tests, keywords, data, and log and report files live under
a folder named **robot**, at the root of your repository. If you worked
through the CumulusCI :doc:`tutorial`, the following folders will
have been created under **MyProject/robot/MyProject**:

- **doc** - a place to put documentation for your tests
- **resources** - a place to put robot libraries and keyword files that
  are unique to your project
- **results** - a place for robot to write its log and report files
- **tests** - a place for all of your tests.


Part 2: Creating a custom object
================================

For this tutorial we're going to use a custom object named
``MyObject`` (e.g. ``MyObject__c``). In addition, we need a custom tab that is associated
with that object.

If you want to run the tests and keywords in this tutorial verbatim,
you will need to go to Setup and create the following:

1. A custom object with the name ``MyObject``.
2. A custom tab associated with this object.


Part 3: Creating and running your first robot test
==================================================

The first thing we want to do is create a test that verifies
we can get to the listing page of the custom object. This will
let us know that everything is configured properly.

Open up your favorite editor and create a file named ``MyObject.robot``
in the folder ``robot/MyProject/tests``. Copy and paste the
following into this file, and then save it.

.. code-block:: robotframework

    *** Settings ***
    Resource  cumulusci/robotframework/Salesforce.robot
    Library   cumulusci.robotframework.PageObjects

    Suite Setup     Open test browser
    Suite Teardown  Delete records and close browser

    *** Test Cases ***
    Test the MyObject listing page
        Go to page  Listing  MyObject__c
        Current page should be  Listing  MyObject__c

.. note::

   The above code uses ``Go to page`` and ``Current page should be``
   which accept a page type (``Listing``) and object name
   (``MyObject__c``). Even though we have yet to create that page object,
   the keywords will work by using a generic implementation. Later,
   once we've created the page object, the test will start using our
   implementation.

To run just this test, run the following command at the prompt:

.. code-block:: console

    $ cci task run robot -o suites robot/MyProject/tests/MyObject.robot --org dev

If everything is set up correctly, you should see the output that
looks similar to this:

.. code-block:: console

    $ cci task run robot -o suites robot/MyProject/tests/MyObject.robot --org dev
    2019-05-21 17:29:25: Getting scratch org info from Salesforce DX
    2019-05-21 17:29:29: Beginning task: Robot
    2019-05-21 17:29:29:        As user: test-wftmq9afc3ud@example.com
    2019-05-21 17:29:29:         In org: 00Df0000003cuDx
    2019-05-21 17:29:29:
    ==============================================================================
    MyObject
    ==============================================================================
    Test the MyObject listing page                                        | PASS |
    ------------------------------------------------------------------------------
    MyObject                                                              | PASS |
    1 critical test, 1 passed, 0 failed
    1 test total, 1 passed, 0 failed
    ==============================================================================
    Output:  /Users/boakley/dev/MyProject/robot/MyProject/results/output.xml
    Log:     /Users/boakley/dev/MyProject/robot/MyProject/results/log.html
    Report:  /Users/boakley/dev/MyProject/robot/MyProject/results/report.html

Part 4: Creating a page object
==============================

Most projects are going to need to write custom keywords that are
unique to that project. For example, NPSP has a keyword for filling in
a batch gift entry form, EDA has a keyword with some custom logic for
validating and affiliated contact, and so on.

The best way to create and organize these keywords is to place them in
page object libraries. These libraries contain normal Python classes
and methods which have been decorated with the ``pageobjects``
decorator provided by CumulusCI. By using page objects, you can write
keywords that are unique to a given page, making them easier to find
and easier to manage.


Defining the class
------------------

CumulusCI provides the base classes that are a good starting point for
your page object (see :ref:`page-object-base-classes`). In this case
we're writing a keyword that works on the listing page, so we want our
class to inherit from the ``ListingPage`` class.

.. note::

    Our class also needs to use the ``pageobject`` decorator, so we must
    import that along with the ``ListingPage`` class.

To get started, create a new file named **MyObjectPages.py** in the
folder ``robot/MyProject/resources``. At the top of the new keyword
file, add the following import statement:

.. code-block:: python

    from cumulusci.robotframework.pageobjects import pageobject, ListingPage

Next we can create the class definition by adding the following two
lines:

.. code-block:: python

    @pageobject(page_type="Listing", object_name="MyObject__c")
    class MyObjectListingPage(ListingPage):

The first line registers this class as a page object for a listing page
for the object ``MyObject__c``. The second line begins the class
definition.

Creating the keyword
--------------------

At this point, all we need to do to create the keyword is to create a
method on this object. The method name should be all lowercase, with
underscores instead of spaces. When called from a robot test, the case
is ignored and all spaces are converted to underscores.

In this case we want to create a method named
``click_on_the_row_with_name``. All we want it to do is to find a
link with the given name, click on the link, and then wait for the new
page to load. To make the code more bulletproof, it will use a keyword
from SeleniumLibrary to wait until the page contains the link before
clicking on it. While probably not strictly necessary on this page,
waiting for elements before interacting with them is a good habit to
get into.

Add the following under the class definition:

.. code-block:: python

    def click_on_the_row_with_name(self, name):
        xpath='xpath://a[@title="{}"]'.format(name)
        self.selenium.wait_until_page_contains_element(xpath)
        self.selenium.click_link(xpath)
        self.salesforce.wait_until_loading_is_complete()

Notice that the above code is able to use the built-in properties
``self.selenium`` and ``self.salesforce`` to directly call keywords in
the ``SeleniumLibrary`` and ``Salesforce`` keyword libraries.


Putting it all together
-----------------------

After adding all of the above code, our file should now look like
this:

.. code-block:: python

    from cumulusci.robotframework.pageobjects import pageobject, ListingPage


    @pageobject(page_type="Listing", object_name="MyObject__c")
    class MyObjectListingPage(ListingPage):
        def click_on_the_row_with_name(self, name):
            xpath='xpath://a[@title="{}"]'.format(name)
            self.selenium.wait_until_page_contains_element(xpath)
            self.selenium.click_link(xpath)
            self.salesforce.wait_until_loading_is_complete()

We now need to import this page object into our tests. In the first
iteration of the test, we imported
``cumulusci.robotframework.PageObjects``, which provided our test with
keywords such as ``Go to page`` and ``Current page should be``. In
addition to being the source of these keywords, it is also the way to
import page object files into a test case.

To import a file with one or more page objects you need to supply the
path to the page object file as an argument when importing
``PageObjects``. The easiest way is to use robot's continuation
characters ``...`` on a separate line.

Modify the import statements at the top of ``MyObject.robot`` to look
like the following:

.. code-block:: robotframework

    *** Settings ***
    Resource  cumulusci/robotframework/Salesforce.robot
    Library   cumulusci.robotframework.PageObjects
    ...  robot/MyProject/resources/MyObjectPages.py

This will import the page object definitions into the test case, but
the keywords won't be available until the page object is loaded. Page
objects are loaded automatically when you call ``Go to page``, or you
can explicitly load them with ``Load page object``. In both cases, the
first argument is the page type (eg: `Listing`, `Home`, etc) and the
second argument is the object name (eg: ``MyObject__c``).

Our test is already using ``Go to page``, so our keyword should
already be available to us once we've gone to that page.


Part 5: Adding test data
========================

We want to be able to test that when we click on one of our custom
objects on the listing page that it will take us to the detail page
for that object. To do that, our test needs some test data. While that
can be very complicated in a real-world scenario, for simple tests we
can use the Salesforce API to create test data when the suite first
starts up.

To create the data when the suite starts, we can add a ``Suite Setup``
in the settings section of the test. This takes as an argument the
name of a keyword. In our case we're going to create a custom keyword
right in the test to add some test data for us.

It is not necessary to do it in a setup. It could be a step in an
individual test case, for example. However, putting it in the ``Suite
Setup`` guarantees it will run before any tests in the same file are
run.

Open up ``MyObject.robot`` and add the following just before ``***
Test Cases ***``:

.. code-block:: robotframework

    *** Keywords ***
    Create test data
        [Documentation]
        ...  Creates a MyObject record named "Leeroy Jenkins"
        ...  if one doesn't exist

        # Check to see if the record is already in the database,
        # and return if it already exists
        ${status}  ${result}=  Run keyword and ignore error  Salesforce get  MyObject__c  Name=Leeroy Jenkins
        Return from keyword if  '${status}'=='PASS'

        # The record didn't exist, so create it
        Log  creating MyObject object with name 'Leeroy Jenkins'  DEBUG
        Salesforce Insert  MyObject__c  Name=Leeroy Jenkins

We also need to modify our ``Suite Setup`` to call this keyword in
addition to calling the ``Open Test Browser`` keyword. Since ``Suite
Setup`` only accepts a single keyword, we can use the built-in keyword
``Run keywords`` to run more than one keyword in the setup.

Change the suite setup to look like the following, again using robot's
continuation characters to spread the code across multipe rows for
readability.

.. note::

    It is critical that you use all caps for ``AND``, as
    that's the way robot knows where one keyword ends and the next
    begins.

.. code-block:: robotframework

    Suite Setup     Run keywords
    ...  Create test data
    ...  AND  Open test browser

Notice that our ``Suite Teardown`` calls ``Delete records and
close browser``. The _records_ in that keyword refers to any data
records created by ``Salesforce Insert``. This makes it possible to
both create and later clean up temporary data used for a test.

It is important to note that the suite teardown isn't guaranteed to run
if you forcibly kill a running robot test. For that reason, we added a
step in ``Create test data`` to check for an existing record
before adding it. If a previous test was interrupted and the record
already exists, there's no reason to create a new record.


Part 6: Using the new keyword
=============================

We are now ready to modify our test to use our new keyword, since we
now have some test data in our database, and the keyword definition in
our page object file.

Once again, edit ``MyObject.robot`` to add the following two
statements at the end of our test:

.. code-block:: robotframework

    Click on the row with name  Leeroy Jenkins
    Current page should be  Detail  MyObject__c

The complete test should now look like this:

.. code-block:: robotframework

    *** Settings ***
    Resource  cumulusci/robotframework/Salesforce.robot
    Library   cumulusci.robotframework.PageObjects
    ...  robot/MyProject/resources/MyObjectPages.py

    Suite Setup     Run keywords
    ...  Create test data
    ...  AND  Open test browser
    Suite Teardown  Delete records and close browser

    *** Keywords ***
    Create test data
        [Documentation]  Creates a MyObject record named "Leeroy Jenkins" if one doesn't exist

        # Check to see if the record is already in the database,
        # and do nothing if it already exists
        ${status}  ${result}=  Run keyword and ignore error  Salesforce get  MyObject__c  Name=Leeroy Jenkins
        Return from keyword if  '${status}'=='PASS'

        # The record didn't exist, so create it
        Log  creating MyObject object with name 'Leeroy Jenkins'  DEBUG
        Salesforce Insert  MyObject__c  Name=Leeroy Jenkins

    *** Test Cases ***
    Test the MyObject listing page
        Go to page  Listing  MyObject__c
        Current page should be  Listing  MyObject__c

        Click on the row with name  Leeroy Jenkins
        Current page should be  Detail  MyObject__c

With everything in place, we should be able to run the test using the
same command as before:

.. code-block:: console

    $ cci task run robot -o suites robot/MyProject/tests/MyObject.robot --org dev
    2019-05-21 22:02:27: Getting scratch org info from Salesforce DX
    2019-05-21 22:02:31: Beginning task: Robot
    2019-05-21 22:02:31:        As user: test-wftmq9afc3ud@example.com
    2019-05-21 22:02:31:         In org: 00Df0000003cuDx
    2019-05-21 22:02:31:
    ==============================================================================
    MyObject
    ==============================================================================
    Test the MyObject listing page                                        | PASS |
    ------------------------------------------------------------------------------
    MyObject                                                              | PASS |
    1 critical test, 1 passed, 0 failed
    1 test total, 1 passed, 0 failed
    ==============================================================================
    Output:  /Users/boakley/dev/MyProject/robot/MyProject/results/output.xml
    Log:     /Users/boakley/dev/MyProject/robot/MyProject/results/log.html
    Report:  /Users/boakley/dev/MyProject/robot/MyProject/results/report.html
