Constructing Custom Locators in OpenTest
Tags: opensource, testing, android, javascript
Published: 2021-10-24 03:28:48

On a previous project, I got to use a nifty tool called [OpenTest](https://github.com/mcdcorp/opentest) to orchestrate test automation on an Android app. It's not a widely popular framework yet, but I hope it grows because it solves one particular problem quite well. OpenTest enables a QA team to write tests _once_ that can run across multiple environments and platforms. It's a solid tool if you're looking to do something like automate an iOS and Android app that have identical UIs, or just want a shared language for your web and mobile app testing frameworks. OpenTest is also written in YAML, so it's approachable for even quite junior SDET teams, and it contains a lot of keyword-based actions that make UI tests simple to compose.

In most cases, you simply structure tests like this:

```yaml
- description: Verify Text is Accurate
  action: org.getopentest.appium.AssertElementText
  args:
    locator: $data("locators/view").area.element
    text: 'Whatever text I expect to see'
```

In most cases, you can locate elements by their `id` or `text` properties, but in cases where you need to construct custom locators on the fly, it can be useful to know how OpenTest interprets Javascript.

Javascript can be dynamically inserted into OpenTest tests using the `script` tag, and if a bar (`|`) is inserted on the first line, then everything following it is interpreted as Javascript, so you can use multi-line Javascript blocks. For more on scripting support in OpenTest, [read the docs](https://getopentest.org/docs/scripting-support.html).

For now, all you need to know is that Javascript can be dynamically inserted anywhere in a test, and that you can call all your basic OpenTest [actions](https://getopentest.org/reference/appium-keywords.html#keyword-list) like `AssertElementText` by using the special `$runAction` command in your Javascript blocks.

Let's imagine an example interface for an administrator viewing an Employee Management System that lists employees and has a "Reset Password" button next to their names. Let's say there are just two employees in the system, Taz and Julia.

In this example, we have locators in our OpenTest `/data` repository that correspond to the elements in the interface.

Taz and Julia show up in my Employee Management System, and next to each of their names, I can see a button to reset their passwords. We're going to have two separate locators for the two "Password Reset" buttons. To test this, I need to find the row with their name in it, and _then_ find the corresponding password reset button to select. We can use _string concatentation_ to directly insert each person's name into the xpath locator.

```yaml
tazBtn:   { xpath: "//ClassName[@resource-id='personRow' and @text='Taz']/following-sibling::android.widget.Button[@resource-id='passwordResetButton']" }
juliaBtn: { xpath: "//ClassName[@resource-id='personRow' and @text='Julia']/following-sibling::android.widget.Button[@resource-id='passwordResetButton']" }
```

OpenTest does a good job of separating test logic from test data. References to data strings, numbers, and other values can be included in tests using references formatted like: `$data("fileName").parent.child`.

Here's an example of declaring a text label directly, and then using the value concatenated into a locator identifier.

```javascript
- description: Verify Text Label
  script: |
    var label = "Some text.";
    $runAction("org.getopentest.appium.AssertElementVisible", 
    {
    locator: "//android.widget.TextView[@text='" + label + "']"
    });
```

It's important to remember that direct string concatenation will not work in your locator strings, you'll need to explicitly declare a new variable for the string you'll use at the top of our Javascript block.

Here's an example that shows how to structure some code that relies on inserting a `$data` value into a `text` property so we can type it into a field. In this case, we're pulling from a list of products in our `$data` directory.

```javascript
- description: Enter product name into text field
  script: |
      var textToEnter = $data("products").product1.name
      $runAction("org.getopentest.appium.SendKeys", {
        locator: $data("locators/form").input,
        text: textToEnter
      });
```

You might be tempted to define your `text` value directly with the `$data` value, but this won't work and you *must* explicitly define that value first in a variable at the top of your Javascript block.

I hope this saves someone a bit of frustration. If you're using OpenTest on a project, I'd love to hear what you think of it on [Twitter](https://twitter.com/samelawrence).