JSON Grammar
Root Element
The root of a page object can contain these properties:
elements
(Optional) Array. A nested tree of element objects. An element object can be one of these types:- Basic element—A UI element required for user interaction or to scope another element.
- Custom element—A nested page object.
- Container element—A
slot
ordiv
into which other components are injected.
methods
(Optional) Array. Each object declares a public method. The only supported method type iscompose
. A compose method combines several element actions, like clearing a text field (clear
) and entering a value (setText
).root
(Optional) Boolean. To load a page object from a test, setroot
totrue
and add aselector
element that points to its most outer (root) element. Any page object that is unique inside the current browser view can be used as a root. A root page object can be loaded directly inside the browser. If a component can be loaded only inside its immediate parent (for example, a navigation item can be loaded only inside a navigation bar), don’t mark it asroot
.selector
(Optional) Object. Ifroot
istrue
, add aselector
that points to its most outer (root) element. The root selector must match the HTML tag name. See selector.shadow
(Optional) Object. A shadow boundary at the root of the page object. Contains only anelements
property, which is a nested tree of objects. A page object can haveelements
scoped both inside and outside its shadow root.exposeRootElement
(Optional) Boolean. See Actionable Root.type
(Optional) String. See Actionable Root.platform
(Optional) String. See Platform Context.description
(Optional) String or Object. See Page Object Description.metadata
(Optional) Object. See Page Object Metadata.
{
"elements": [],
"methods": [],
"shadow": {
"elements": []
},
"root": true,
"selector": {
"css": "one-record-home"
}
}
Page Object Description
To help a test writer understand when and how to use a page object, add a description at the root level. It's an optional field that can have a simplified string format, or an object format for longer descriptions.
Here's an example of a simplified string description
property:
{
"description" : "Selector: lightning-accordion. Represents the lightning-accordion Lightning web component. Access a list of all accordion sections or select a section by its title."
}
Tip: It's a good idea to reference the page object root selector for easier search.
Here's a code snippet of a generated page object, which includes the description in its JavaDoc or JSDoc:
/**
* Selector: lightning-accordion. Represents the lightning-accordion Lightning web component.
* Access a list of all accordion sections or select a section by its title.
*
* @author UTAM
* @version 2022-02-26 19:40:45
*/
If the description is long or if the developer also wants to point to the author of the page object, the description can be an object:
{
"description" : {
"text" : [
"very",
"long",
"text"
],
"author" : "my scrum team",
"deprecated" : "since Summer '22 release, use MyOtherComponent"
}
}
text
is an array of stringsauthor
(Optional) is a string with the team name that developed and owns a page objectdeprecated
(Optional) if a page object is no longer supported, mark it as deprecated. This string property explains why and what to use instead.
Here's a code snippet of a generated page object, which includes the description in its JavaDoc or JSDoc:
/**
* very
* long
* text
*
* @author my scrum team
* @version 2022-02-26 19:40:45
* @deprecated since Summer'22 release, use MyOtherComponent
*/
Page Object Metadata
To help organizations with a large number of page objects track miscellaneous information for each
page object, you can add a metadata
object property. This property is optional and its contents
are entirely ignored by the compiler; however, if you define this property in a page object, the
value must be a JSON object. Use the property to track whatever information is useful to you.
This example uses the metadata
property to track the status
and teamOwner
for a page object.
{
"metadata": {
"status": "In Development",
"teamOwner": "Test Automation Team"
}
}
Actionable Root Element
To make the root element actionable, expose it via a public method. Add these properties to the root element:
exposeRootElement
Boolean. If set totrue
, UTAM creates a public method that returns an instance of the element with the given type. The name of the getter isgetRoot
.type
(Optional) See basic element types.
{
"exposeRootElement": true,
"type": ["editable"],
"elements": [],
"methods": []
}
The UTAM generator converts this JSON into a public method. The method returns an instance of the page object root element for the test to interact with.
async getRoot() {
const driver = this.driver;
const root = await this.getRootElement();
return new _EditableUtamElement(driver, root);
}
Note: The word
root
is reserved and can't be used as an element name.
Basic Element
If a component has an HTML element that you need in the page object, declare a basic element as an object in an elements
array. Add a basic element only if it’s needed for user interactions or to scope other elements.
You can nest a basic element object in any elements
array: at the root, inside a shadow, or nested inside another basic element.
A basic element can have these properties:
-
elements
(Optional) Array. Contains a nested tree of element objects that are located inside this basic element. -
filter
(Optional) Object. Picks an element from a list or filters a list at run time. See Element Filter. -
name
(Required) String. The element name, which UTAM uses in the getter method name. The value must be unique within the JSON file. -
nullable
(Optional, default isfalse
). Boolean. If set totrue
and the element can't be found inside its parent, the getter method returnsnull
. -
public
(Optional, default isfalse
) Boolean. If set totrue
, UTAM generates a public method that returns an instance of the element with the given type. The name of the getter is generated automatically from thename
property asget<Name>
(the value of thename
property is capitalized). -
selector
(Required) Object. Locates the element inside its immediate parent. See Selector properties. -
shadow
(Optional) Object. A shadow boundary. Contains only anelements
property, which is a nested tree of objects. -
type
(Optional) A String for a single type or an array of strings. A list of the types of user interaction that this basic element supports. If omitted, it defaults to a base element type, which supports the methods listed in the Base Element Actions table in actions.To allow more interaction, add one or more of these values to the type array:
actionable
Exposes the methods listed in the Actionable Type Actions table.clickable
Exposes the methods listed in the Clickable Type Actions table.editable
Exposes the methods listed in the Editable Type Actions table.draggable
Exposes the methods listed in the Draggable Type Actions table.touchable
Exposes the methods for mobile device interactions listed in the Touchable Type Actions table.
{
"elements": [
{
"name": "myElement",
"type" : [ "clickable", "editable" ],
"public": true,
"selector": {
"css": ".element"
},
"elements": []
}
]
}
If an element has one basic type only, it can be defined as a string instead an array of strings. For example, this root element has the actionable
type only:
{
"exposeRootElement" : true,
"type" : "actionable"
}
UTAM generates a public method that returns an instance of the element to interact with.
Java:
public MyElementElement getMyElement() {
// return element
}
JavaScript:
//declaration
getMyElement(): Promise<_BaseUtamElement>;
// implementation
async getMyElement() {
const driver = this.driver;
const root = await this.getRootElement();
let element = await _utam_get_myElement(driver, root, );
return new _ActionableUtamElement(driver, element);
}
Custom Element
To represent a nested component, declare a custom element as an object in the elements
array. A custom element has a type
property that references another page object.
You can nest a custom element object in any elements
array: at the root, inside a shadow, or nested inside a basic element.
A custom element can't have nested elements.
A custom element has the same properties as a basic element, except that the type
property is required and must reference another page object.
-
type
(Required) String. A reference to a UTAM page object. The format is:<package name>/pageObjects/<component namespace>/<page object name>
For JavaScript, the
<package name>
and the<page object name>
are required and can include only alphanumeric characters and dashes. The path between these segments is optional. Each path segment can contain only alphanumeric characters (no dashes).For Java, the
<package name>
must start withutam-
and the/pageObjects/
segment is required. The<page object name>
must be a valid class name (it can't include dashes). It can start with a lowercase character, because the compiler transforms it to uppercase.For Java, the UTAM compiler transforms the
type
value to match Java syntax rules.-
<package name>
: transform-
into.
-
Path: transform
/
into.
; transform uppercase characters into lowercase. -
<page object name>
: transform lowercase first letter to uppercase, because Java class names always start with an uppercase character.// JSON "type": "utam-navex/pageObjects/one/navigationBar" // UTAM compiler transforms to: utam.navex.pageobjects.one.NavigationBar
-
This example declares a custom element called todo-item
, which lives in the utam-tutorial
package.
{
"root": true,
"selector": { "css": "body" },
"elements": [
{
"name": "todoApp",
"selector": { "css": "example-todo-app" },
"public": true,
"shadow": {
"elements": [
{
"name": "todoItem",
"selector": { "css": "example-todo-item" },
"public": true,
"type": "utam-tutorial/todoItem"
}
]
}
}
]
}
The generated getTodoItem()
method returns an object scoped inside its parent element.
JavaScript:
getTodoApp(): Promise<_ActionableUtamElement>;
getTodoItem(): Promise<_todoItem>;
Container Element
A component with a slot
or div
can act as a container for other components whose types are unknown to the developer.
There are certain requirements to declare a container element for a slot. For more information, see the guidelines in the slots guide.
For example, lightning-tabset
has a placeholder for tab content. In component source code, a placeholder is a slot
.
<template>
<div class={computedClass} title={title}>
<lightning-tab-bar variant={variant} onselect={handleTabSelected}></lightning-tab-bar>
<slot></slot>
</div>
</template>
Another example is a panel component that can hold any content and produces HTML like this:
<div class="actionBody">
<!-- any component can be injected here -->
<records-detail-panel>
<!-- inner HTML -->
</records-detail-panel>
<div>
In this case, declare an element with "type": "container"
. The compiler generates a method with a parameter for the type of the component being loaded.
You can nest a container object in any elements
array: at the root, inside a shadow, or nested inside a basic element.
To declare a container element:
- If needed, add a private basic element that serves as a scope. (Don't set
"public": true
on the scope element; the test writer doesn't use this element in a test and doesn't need to know about it.)
In our examples:
- For the container in the panel, the scope element is
<div class="actionBody">
- For the container in
lightning-tabset
, the scope element doesn't exist. The container is placed directly in thelightning-tabset
root.
- Add a nested public container element with these properties:
name
(Required) String. An element name that's unique to this JSON file.type
(Required) String. The value must becontainer
.public
(Required) Boolean. Must be set totrue
so that UTAM generates a public method.selector
(Optional, default is"css": ":scope > *:first-child"
). A selector injected as a root for the container content.
- As with any other element, if there's a
#shadow-root
between the basic element and the nested container element, enclose the container element in ashadow
object.- Most containers should have a hardcoded selector. In most cases, the hardcoded selector should be
*
, which represents all the direct children of container's enclosing parent element.- If a selector is omitted, a default CSS selector of
:scope > *:first-child
will be used in the generated code.
Let's take our panel, which has body and footer areas where a developer can render any application component. In the page object, add a private HTML element contentScope
that serves as a scope. Then add a nested public panelContent
container element.
{
"elements": [
{
"name": "contentScope",
"selector": {
"css": ".actionBody"
},
"elements": [
{
"name": "panelContent",
"type": "container",
"public": true,
"selector": {
"css": "*"
}
}
]
}
]
}
UTAM generates this JavaScript code.
async function _utam_get_actionBody(driver, root) {
let _element = root;
const _locator = core.By.css(`.actionBody`);
return _element.findElement(_locator);
}
async function _utam_get_detailsPanelContainer(driver, root) {
let _element = await _utam_get_actionBody(driver, root, );
const _locator = core.By.css(`*`);
return _element.findElement(_locator);
}
async getPanelContent(ContainerCtor) {
const driver = this.driver;
const root = await this.getRootElement();
let element = await _utam_get_detailsPanelContainer(driver, root, );
element = new ContainerCtor(driver, element);
return element;
}
From the test, to load DetailsPanel
inside our panel, call the generated container method.
const recordModal = await utam.load(MyPanel);
const detailPanel = await recordModal.getPanelContent(DetailsPanel);
Sometimes the container selector can be more specific than *
. For the lightning-tabset
component we know that each tab has an attribute role, so the selector can be [role="tabpanel"]
. But if the container is placed correctly inside its immediate parent, *
always works.
If the container declaration doesn't have a selector, the generated method has a second parameter for a selector to be hardcoded from test code. We consider this a bad practice because it exposes internals of the component, but it is possible.
const detailPanel = await recordModal.getPanelContent(DetailsPanel, utam.By.css('records-detail-panel'));
If the selector inside the container is marked with "returnAll": true
, the generated container method returns an array (in JavaScript) or a list (in Java) of objects.
For our tabset example it makes sense to return all tabs:
{
"elements": [
{
"type": "container",
"name": "tabs",
"public": true,
"selector": {
"css": "[role='tabpanel']",
"returnAll": true
}
}
]
}
UTAM generates this JavaScript code.
// declaration returns array
getTabs<T extends _UtamBasePageObject>(ContainerCtor: _ContainerCtor<T>): Promise<T[]>;
// implementation
async getTabs(ContainerCtor) {
const driver = this.driver;
const root = await this.getRootElement();
let elements = await _utam_get_slots(driver, root, );
elements = elements.map(function _createUtamElement(element) {
return new ContainerCtor(driver, element);
});
return elements;
}
The test method loads all the existing tabs and returns an array with 3 tabs:
const tabs = await tabset.getTabs(Tab);
expect(labs.length).toBe(3);
Frame Element
To support a frame or an iframe, use a separate type of element with the following properties:
name
(Required) String. The element name, which UTAM uses in the getter method name. The value must be unique within the JSON file.type
(Required) String. Set toframe
and extends Basic element type.public
(Optional, default isfalse
) Boolean. If set totrue
, UTAM generates a public method that returns an instance of the element with the given type. The name of the getter is generated automatically from thename
property asget<Name>
(the value of thename
property is capitalized).selector
(Required) Object. Locates the element inside its immediate parent. See Selector properties. Note thatreturnAll
inside a selector isn't allowed for a frame.
Here's an example of a frame element:
{
"name": "myPublicFrame",
"public": true,
"type": "frame",
"selector": {
"css": "iframe"
}
}
The generated code returns an object of the special FrameElement
type that can be used as a parameter in methods to switch between frames.
Element Selector
Every basic element and custom element requires a selector
object that locates the element at run time. A container element doesn't require a selector
, because it has a default.
When a selector is used for a custom element, the selector
is reserved for the custom element’s type and can’t be reused for any other type.
UTAM supports all valid CSS selectors.
Selector Properties
A selector
object has these properties:
css
(Required) String. A standard CSS selector, which can also include special selector parameters. UTAM supports all valid CSS selectors.args
(Optional) Array. Parameters to add to the methods that access this element or its nested elements. Each element in the array has these properties:name
(Required) String. A parameter name that is unique in the scope of theargs
array.type
(Required) String. A primitive type, eitherstring
,number
orboolean
.
returnAll
(Optional, default isfalse
) Boolean. To get a list of elements, setreturnAll
totrue
.
{
"elements": [
{
"name": "myDiv",
"selector": {
"css": ".myDiv"
}
},
{
"name": "listItems",
"selector": {
"css": "li a",
"returnAll": true
}
}
]
}
Selector Parameters
A selector
can depend on run-time information from the test, like a label or an index. To access this run-time information, use these format indicators inside a CSS selector. At run time, they’re replaced by values passed as parameters to the element’s public method.
- String (
%s
) - Integer (
%d
)
{
"elements": [
{
"name": "listItemWithTitle",
"selector": {
"css": "li a[title='%s']",
"args": [
{
"name": "titleString",
"type": "string"
}
]
},
"public": true
}
]
}
UTAM generates this code from the JSON.
public Actionable getListItemWithTitle(String titleString) {
// return instance of the element using selector with replaced parameter
}
A nested element can inherit parameters from its parents. In this example, the nested input needs two integer parameters for the enclosing table cell.
{
"elements": [
{
"name": "tableCell",
"selector": {
"css": "tr:nth-of-type(%d) td:nth-of-type(%d)",
"args": [
{
"name": "rowIndex",
"type": "number"
},
{
"name": "colIndex",
"type": "number"
}
]
},
"elements": [
{
"name": "inputInsideCell",
"selector": {
"css": "input"
},
"type": ["editable"],
"public": true
}
]
}
]
}
UTAM collects parameter names for every element from root that leads to the current element and adds them in order of appearance.
public Editable getInputInsideCell(int rowIndex, int colIndex) {
// Apply the provided parameters,
// then find an input element inside a table cell.
}
Mobile Selectors
The Salesforce mobile app is a mobile hybrid application. Some pages are WebView pages, and some pages are native pages. For a native page, CSS selectors aren't sufficient. To find elements in a mobile environment, Appium implements a number of locator strategies for specific mobile devices. For more information, see appium.io: Find Elements.
Appium is an open-source tool for automating native, mobile web, and hybrid applications. Appium allows you to write tests against multiple platforms, such as iOS and Android, using the same API.
Accessibility ID Selector
This selector is supported for Mobile Platform only.
Use the accessibility ID selector, which is a unique identifier for a UI element. This example uses an accessibility ID selector of searchCell.ViewAllCell
.
{
...
"name": "searchCell",
"selector": {
"accessid": "searchCell.ViewAllCell"
},
...
}
Generated Java code:
@Selector.Find(accessid = "searchCell.ViewAllCell")
private ElementLocation searchCell;
iOS Class Chain Selector
This selector is for iOS Platform only.
The iOS class chain selector finds a window element from the root of the page:
Page object JSON:
{
...
"name": "appWindow",
"selector": {
"classchain": "XCUIElementTypeWindow"
},
...
}
Generated Java code:
@Selector.Find(classchain = "XCUIElementTypeWindow")
private ElementLocation appWindow;
You can use double star and slash (**/) to define the next item descendant from the root. This strategy makes the configured locator not sensitive to the hierarchy of the page.
Page object json:
{
...
"name": "button",
"selector": {
"classchain": "**/XCUIElementTypeButton"
},
...
}
Generated Java code:
@Selector.Find(classchain = "**/XCUIElementTypeButton")
private ElementLocation button;
Sometimes, the element type isn't unique enough to identify a specific element. In those scenarios, you can combine the element type with an attribute using a predicate. The predicate string should always be enclosed in ` marks or $
characters inside square brackets. Use `` or $$ to escape a single ` or $
character inside a predicate expression.
A single backtick means the predicate expression is applied to the current children. A single dollar sign means the predicate expression is applied to all the descendants of the current element(s). String values, not the key words, can be enclosed by ' or ".
Page object JSON:
{
...
"name": "newButton",
"selector": {
"classchain": "**/XCUIElementTypeButton[`name BEGINSWITH 'New'`]",
},
...
}
Generated Java code:
@Selector.Find(classchain = "**/XCUIElementTypeButton[`name BEGINSWITH 'New'`])
private ElementLocation newButton;
This selector uses the $
character to apply the attribute to all the descendants of the current element(s).
Page object JSON:
{
...
"name": "newButton",
"selector": {
"classchain": "**/XCUIElementTypeCell[$name == \"My Accounts\"$]",
},
...
}
Generated Java code:
@Selector.Find(classchain = "**/XCUIElementTypeCell[$name == \"My Accounts\"$])
private ElementLocation newButton;
The following attributes are supported:
name
visible
value
enabled
The following comparison types for an attribute are supported:
EQUALS
or==
BEGINSWITH
ENDSWITH
CONTAINS
You can use the following operators to set conditions for multiple attributes in one expression.
AND
OR
For example:
{
...
"name": "newButton",
"selector": {
"classchain": "**/XCUIElementTypeBotton[`name BEGINSWITH 'New' AND visible == true`]",
},
...
}
Generated Java code:
@Selector.Find(
classchain = "**/XCUIElementTypeBotton[`name BEGINSWITH 'New' AND visible == true`]"
)
private ElementLocation newButton;
Similarly to CSS selectors, you can use string or integer parameters. This example uses a string parameter.
{
...
"name": "tabBarBtn",
"selector": {
"classchain": "**/XCUIElementTypeButton[`name == '%s'`]",
"args": [
{
"name": "item",
"type": "string"
}
]
}
}
Generated Java code:
@ElementMarker.Find(classchain = "**/XCUIElementTypeButton[`name == '%s'`]")
private ElementLocation tabBarBtn;
@Override
final TabBarBtnElement getTabBarBtnElement(String item) {
return element(this.tabBarBtn).build(TabBarBtnElement.class, TabBarBtnElementImpl.class, item);
}
Here's more detailed examples for iOS Class Chain.
Android UIAutomator Selector
This selector is for Android Platform only.
Appium uses the UIAutomator selector to enable searching using UiSelectors. We support the following methods only from UiSelector
to find elements:
checkable(boolean val)
checked(boolean val)
className(String className)
clickable(boolean val)
description(String desc)
descriptionContains(String desc)
descriptionStartsWith(String desc)
enabled(boolean val)
selected(boolean val)
resourceId (String id)
resourceIdMatches(String regex)
This example uses the clickable
method:
{
...
"name": "acceptButton",
"selector": {
"uiautomator": "new UiSelector().clickable(true)"
},
...
}
Generated Java code:
@Selector.Find(uiautomator = "new UiSelector().clickable(true)")
private ElementLocation acceptButton;
This example uses the className
method:
{
...
"name": "newButton",
"selector": {
"uiautomator": "new UiSelector().className(\"android.widget.TextView\")"
},
...
}
Generated Java code:
@Selector.Find(
uiautomator = "new UiSelector().className(\"android.widget.TextView\")"
)
private ElementLocation newButton;
This example uses the descriptionContains
method:
{
...
"name": "vfLink",
"selector": {
"uiautomator": "new UiSelector().descriptionContains(\"VfPage Link\")"
},
...
}
Generated Java code:
@Selector.Find(
uiautomator = "new UiSelector().descriptionContains(\"VfPage Link\")"
)
private ElementLocation vfLink;
UIScrollable
is a powerful Android class that performs element lookups in scrollable layouts. In most cases, you should use the scrollIntoView
method, which performs a scroll action until the destination element is found on the screen. You can use UIScrollable
swipe to:
- search elements in a list (for example in a country list)
- search elements outside of the screen (for example, an input field, text, or button)
The scrollIntoView
method has UiSelector as search criteria input to allow you to find elements by supported methods. For example:
{
"implements": "utam-salesforceapp/pageObjects/navigation/navItemsList",
"elements": [
{
"name": "navItemWithLabel",
"selector": {
"uiautomator": "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(className(\"android.widget.TextView\").text(\"%s\"))",
"args": [
{
"name": "item",
"type": "string"
}
]
}
}
The generated Java code is:
@ElementMarker.Find(
uiautomator =
"new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(className(\"android.widget.TextView\").text(\"%s\"))",
nullable = true
)
private ElementLocation navItemWithLabel;
Here's more details for UIAutomator UiSelector and UiScrollable.
List and Index
When a selector
includes "returnAll": true
, the generated method returns a list.
In the DOM, a parent can contain multiple instances of a custom element, like lightning-tab
components inside a lightning-tabset
. If the selector
has a returnAll
property set to true
, the method returns a list of instances found at run time.
{
"elements": [
{
"name": "allTabs",
"type": "utam-lightning/pageObjects/lightning/tab",
"selector": {
"css": "lightning-tab",
"returnAll": true
},
"public": true
}
]
}
From the previous JSON, UTAM generates this public method, which returns a list of the page objects of the given type.
public List<Tab> getAllTabs() {
// return list of instances found in runtime
// throw exception if nothing found
}
To get one of the instances by its index, add :nth-of-type(%d)
to the injected selector and the args
property with an index
parameter. :nth-of-type(%d)
is 1-based, not 0-based.
{
"elements": [
{
"name": "myComponent",
"type": "utam-lightning/pageObjects/lightning/myComponent",
"selector": {
"css": "lightning-my-component:nth-of-type(%d)",
"args": [
{
"name": "index",
"type": "number"
}
]
},
"public": true
}
]
}
The generated method finds all the custom elements inside the parent and returns one by index
.
public MyComponent getMyComponent(int index) {
// return nth instance
// if nothing found, or index is out of bounds, throw exception
}
Element Filter
To pick an element or a list of elements based on a condition at run time, add a filter inside an element node. An element with a filter
must have a selector
with returnAll
set to true
.
A filter
object inside an element has these properties:
apply
(Required) String. The method called on the element, which must return a value of the type expected bymatcher
.- For a custom element: the method name declared inside the page object.
- For a basic element: one of the supported actions.
findFirst
(Optional, default isfalse
) Boolean. Iftrue
, returns the first element that matches the filter. Iffalse
, returns a list of elements. If the element is marked asnullable
, the filter can returnnull
if no match is found.matcher
(Required) Object. Defines the filter criteria for the data returned by theapply
method.type
(Required) String. The matcher type for filtering data. For supported types, see Matchers.args
(Optional) Array. If the matcher type requires arguments, pass them in this array.
This example uses a filter to pick one of the lightning-menu-item
instances inside a lightning-button-menu
based on the item text.
{
"elements": [
{
"name": "menuItemByText",
"type": "utam-lightning/pageObjects/lightning/menuItem",
"selector": {
"css": "lightning-menu-item",
"returnAll": true
},
"filter": {
"apply": "getItemText",
"findFirst": true,
"matcher": {
"type": "stringContains",
"args": [
{
"name": "itemText",
"type": "string"
}
]
}
},
"public": true
}
]
}
For the example to work, the lightning-menu-item
page object must declare a public method getItemText
that returns menu item text.
The generated method finds all lightning-menu-item
instances inside a lightning-button-menu
based on the item text. For each item, getItemText
executes and picks the first item that contains the parameter value as a substring. If no match is found, it returns null.
public MenuItem getMenuItemByText(String itemText) {
// find all available menu items
// get text for each item
// pick first item that contains value provided in parameter
}
Example of a basic element filter that returns a list:
{
"elements": [
{
"name": "myElement",
"selector": {
"css": ".myClass",
"returnAll": true
},
"filter": {
"apply": "getText",
"matcher": {
"type": "stringContains",
"args": [
{
"name": "matcherArg",
"type": "string"
}
]
}
},
"public": true
}
]
}
UTAM generates this compiled type:
getMyElement(matcherArg: string): Promise<_ActionableUtamElement[]>;
UTAM generates this compiled JavaScript:
async getMyElement(matcherArg) {
const driver = this.driver;
const root = await this.getRootElement();
let elements = await _utam_get_myElements(driver, root, );
elements = elements.map(function _createUtamElement(element) {
return new _ActionableUtamElement(driver, element);
});
const appliedFilter = await Promise.all(elements.map(el => _utam_filter_myElement(el, matcherArg)));
elements = elements.filter((_, i) => appliedFilter[i]);
return elements;
}
Matchers
Use a matcher to apply a condition at run time:
- To pick an element or a list of elements in an element filter. For an example, see Element Filters.
- To transform the return value of a compose statement.
A matcher is an object with these properties.
type
(Required) String. The matcher type to filter data returned by anapply
method or an element's getter method. These types are supported:isFalse
Matchesfalse
values.isTrue
Matchestrue
values.notNull
Matches values that are not null.stringContains
Matches values containing the string passed to the matcher'sargs
array.stringEquals
Matches values that equal the string passed to the matcher'sargs
array.
args
(Optional) Array. If the matcher type requires arguments, pass them in this array.
This matcher uses the stringContains
matcher type.
"matcher": {
"type": "stringContains",
"args": [
{
"name": "itemText",
"type": "string"
}
]
}
Compose Method
To combine several element actions, such as a method for login that sets a username, sets a password, and clicks a button, declare a compose
method.
A compose
method has these properties:
name
(Required) String. The unique name of the method within this JSON file.description
(Optional) String or Object. See Method Description.compose
(Required) Array. Each object is an element and a method.element
(Optional) String. The name of a basic element from the same JSON file. Defaults toself
, which is a reference to the current page object. When a statement refers to the current page object, it can apply the page object's methods. Specify an element only if you need to operate on or interact with it. For an explicit wait using thewaitFor
keyword, omit the element.apply
(Optional) String. If this property isn't defined, the getter method for theelement
property is called.- Basic element: the name of a supported action to apply to the element.
- Custom element: any public method.
args
(Optional) Array. If theapply
action needs parameters, provide them in an array. Each element of the array has these properties:name
(Required) String. A parameter name that is unique in the scope of theargs
array.type
(Required) String. One of these primitive types:string
,number
, orboolean
.
matcher
(Optional) Object. Defines the filter criteria for the data returned by theapply
method or the element's getter method.type
(Required) String. The matcher type for filtering data. For supported types, see Matchers.args
(Optional) Array. If the matcher type requires arguments, pass them in this array.
returnAll
(Optional) Boolean. Iftrue
, the method returns an array (in JavaScript) or a list (in Java) of objects. The default isfalse
.returnType
(Optional) String. Explicitly set the return type for the method. You can set a primitive type, such as"string"
,"number"
,"boolean"
, or a custom type, such as"my/page/object"
. This property is optional because the return type for a compose method can sometimes be inferred by the UTAM compiler. For more information, see Return Type for a Compose Statement.
args
(Optional) Array. To access an argument passed to a compose method in multiple statements, declare a reusable argument at the method level. For more information, see Argument Reference.
Here are some examples of compose methods:
Invoke action for a basic element
This compose method sets text on the root element and clicks a submit button.
{
"type": ["editable"],
"elements": [
{
"name": "submitBtn",
"type": ["clickable"],
"selector": {
"css": ".submit"
}
}
],
"methods": [
{
"name": "submitForm",
"compose": [
{
"element": "root",
"apply": "setText",
"args": [
{
"type": "string",
"name": "stringToEnter"
}
]
},
{
"element": "submitBtn",
"apply": "click"
}
]
}
]
}
Here's the generated Java code:
public void submitForm(String stringToEnter) {
getRoot().setText(stringToEnter);
submitBtn.click();
}
Here's the generated JavaScript code:
async submitForm(stringToEnter) {
const _statement0 = await this.__getRoot();
await _statement0.setText(stringToEnter);
const _statement1 = await this.__getSubmitBtn();
const _result1 = await _statement1.click();
return _result1;
}
Invoke method from the same page object
The invokeSubmitForm
method simply invokes the submitForm
method declared in the same page object.
{
"elements": [
// ...
],
"methods": [
{
"name": "submitForm",
"compose": [
// ...
]
},
{
"name": "invokeSubmitForm",
"compose": [
{
"apply": "submitForm",
"args": [
{
"type": "string",
"name": "stringToEnter"
}
]
}
]
}
]
}
This pattern is useful if you want to reuse the same method in multiple compose statements. For example, you could call submitForm
from login
and loginWithDeepLink
methods.
Here's the generated JavaScript code:
async invokeSubmitForm(stringToEnter) {
const _result0 = await this.submitForm(stringToEnter);
return _result0;
}
Invoke an element's getter
This compose method invokes the getter for the myCustomComponent
element.
{
"elements": [
{
"name": "myCustomComponent",
"type": "my/custom/component",
"selector": {
"css": "custom-component"
}
}
],
"methods": [
{
"name": "composeGettingCustomElement",
"compose": [
{
"element": "myCustomComponent"
}
]
}
]
}
Here's the generated JavaScript code.
async composeGettingCustomElement() {
const _result0 = await this.__getMyCustomComponent();
return _result0;
}
Invoke method from a different page object
This compose method applies the someUnknownPublicMethod
method to the myCustomComponent
custom element.
{
"elements": [
{
"name": "myCustomComponent",
"type": "my/custom/component",
"selector": {
"css": "custom-component"
}
}
],
"methods": [
{
"name": "invokeCustomElementMethod",
"compose": [
{
"element": "myCustomComponent",
"apply": "someUnknownPublicMethod"
}
]
}
]
}
Note that the compiler can't know if the someUnknownPublicMethod
method actually exists, and what are its return value or parameters. The responsibility to validate those things belongs to the developer or a preruntime compilation step (depending on the setup).
Here's the generated JavaScript code:
async invokeCustomElementMethod() {
const _statement0 = await this.__getMyCustomComponent();
const _result0 = await _statement0.someUnknownPublicMethod();
return _result0;
}
Method Description
A compose method or element can have a description that explains its usage, return value, and parameters. The description is used to generate JavaDoc or JSDoc.
The simplified description
format is one string property:
{
"methods": [
{
"name": "myMethod",
"description": "Gets an attribute of the root element",
"compose": [
{
"element": "root",
"apply": "getAttribute",
"args": [
{
"name": "attrName",
"type": "string"
}
]
}
]
}
]
}
The generated method includes the following Javadoc or JSDoc. By default, the @return
tag contains the inferred return type and the @param
tag has the name and type of the parameter:
/**
* Gets an attribute of the root element
*
* @return String
* @param attrName String
*/
The extended object description
format enables you to describe more information, such as the return value:
{
"methods": [
{
"name": "myMethod",
"description": {
"text": [ "Gets an attribute of the root element" ],
"return": "string with an attribute value",
"throws": "NullPointerException if the attribute name is null",
"deprecated" : "in Summer '22 release"
},
"compose": [
{
"element": "root",
"apply": "getAttribute",
"args": [
{
"name": "attrName",
"type": "string",
"description": "string with attribute name"
}
]
}
]
}
]
}
text
string array describing what the method or element doesreturn
(Optional) string that describes the return valuethrows
(Optional) string that describes a thrown exception and when it's throwndeprecated
(Optional) if the method is no longer supported, this string explains when and why the method was deprecated.
To provide a description for the parameter, we added a
description
property for theattrName
argument. The description is only possible for non-literal (not hardcoded by value) arguments.
The generated method has the following Javadoc or JSDoc:
/**
* Gets an attribute of the root element
*
* @return string with attribute value
* @param attrName string with attribute name to get
* @throws NullPointerException if the attribute name is null
* @deprecated in Summer '22 release
*/
In Java, the generated method is marked with an @Deprecated
annotation.
The same description
format can be added to any element:
{
"elements": [
{
"public": true,
"name": "custom",
"type": "utam/pageObjects/MyCustomObject",
"description": "get area inside table",
"selector": {
"css": "css%s",
"args": [
{
"name": "selectorArg",
"type": "string",
"description": "parameter description"
}
]
}
}
]
}
Return Type for a Compose Statement
See method return types.
Chain Compose Statements
It's possible to "chain" compose statements. A chain applies a method or a getter to the result of the previous statement.
Important: Chains are supported only if the previous statement returns a custom type (another page object).
To apply a method or a getter from the current statement to the result of the previous statement, use the "chain": true
property inside a statement.
The chain methods approach has some disadvantages and limitations:
- Because the referenced elements and methods (except the first) are defined in other JSON files, the UTAM compiler can’t validate the correctness of the chain until the page object is generated. If the element doesn’t exist, if it isn’t public, or if the type is incorrect, the problem is discovered when the generated code is compiled.
- The current page object now depends on the content of other page objects, so changes in those might prevent the page object from compiling. We notice the issue during build, but it's more difficult to manage.
- The page object author might not be aware of the internals. If another team adds a chained method, ownership of the page object is unclear.
Here are some examples of chaining compose statements.
Compose container element with getter and public action
Consider the following test code samples:
const myModal = await utam.load(MyModalWithDynamicContent);
// long version
const footerArea = await myModal.getContent(FooterAreaWrapper);
const footerButtonsPanel = await footerArea.getFooterButtonsPanel();
await footerButtonsPanel.clickButtonByIndex(1);
// short version
await myModal.clickSave();
It's possible to compose the long version into a short version using a chain.
{
"elements": [
{
"name": "content",
"type": "container"
}
],
"methods": [
{
"name": "clickSave",
"compose": [
{
// invoke container method with hardcoded type
"element": "content",
"args": [
{
"type": "pageObject",
"value": "my/pageObjects/FooterAreaWrapper"
}
],
"returnType": "my/pageObjects/FooterAreaWrapper"
},
{
// invoke getter
"chain": true,
"element": "footerButtonsPanel",
"returnType": "my/pageObjects/FooterButtonsPanel"
},
{
// invoke public method
"chain": true,
"apply": "clickButtonByIndex",
"args": [
{
"value": 1
}
]
}
]
}
]
}
Chaining list to a list
If both previous and current statements return lists ("returnAll": true
), the method is applied to each returned element using flatMap
.
Consider the use case of a table with multiple rows and cells. Assume that each row and cell is a separate component. Let's write a method that returns all cells inside a table.
- table row JSON
{
"elements": [
{
"type": "my/pageObjects/tableCell",
"name": "tableCells",
"selector": {
"css": "table-cell",
"returnAll": true
}
}
]
- table JSON
{
"elements": [
{
"type": "my/pageObjects/tableRow",
"name": "tableRows",
"selector": {
"css": "table-row",
"returnAll": true
}
}
],
"methods": [
{
"name": "getAllCells",
"compose": [
{
"returnType": "my/pageObjects/tableRow",
"returnAll": true,
"element": "tableRows"
},
{
"chain": true,
"returnType": "my/pageObjects/tableCell",
"returnAll": true,
"element": "tableCells"
}
]
}
]
}
Generated Java code:
public final List<TableCell> getAllCells() {
List<TableRow> statement0 = this.getTableRowsElement();
List<TableCell> statement1 =
statement0
.stream()
.flatMap(element -> element.getTableCells().stream())
.collect(Collectors.toList());
return statement1;
}
Argument Reference
To access an argument passed to a compose method in multiple statements, declare a reusable argument at the method level. The args
property must be a sibling of the name
property.
For example, we declare a reusable passwordStr
argument at the method level:
{
"name": "passwordStr",
"type": "string"
}
Then in each compose statement where you want to access the reusable argument, set the argument's type
to argumentReference
, the name
of an argument with "type": "argumentReference"
must match the name of one of the reusable arguments declared at the method level:
{
"name": "passwordStr",
"type": "argumentReference"
}
Here's the full method declaration. The statements that apply actions to the password
and passwordConfirmation
elements both declare a reference to the reusable passwordStr
argument. When test code invokes this method, the passwordStr
argument is passed to both statements:
{
"name": "confirmPassword",
"args": [
{
"name": "passwordStr",
"type": "string"
}
],
"compose": [
{
"element": "password",
"apply": "clearAndType",
"args": [
{
"name": "passwordStr",
"type": "argumentReference"
}
]
},
{
"element": "passwordConfirmation",
"apply": "clearAndType",
"args": [
{
"name": "passwordStr",
"type": "argumentReference"
}
]
}
]
}
A reusable argument can't be an argument with a hardcoded value, also known as a literal type.
For a working example of argument references, see this tutorial.
Supported Argument Types
This table shows the syntax for declaring a non-literal argument with a name and type. Instead of using a combination of name and type, it's also possible to use a hardcoded (literal) value for an argument of these types.
Name/type (non literal) | Value/type (literal) | |
---|---|---|
string primitive | { "name": "myArg", "type": "string" } | { "value": "myString" } |
integer primitive | { "name": "myArg", "type": "number" } | { "value": 1024 } |
boolean primitive | { "name": "myArg", "type": "boolean" } | { "value": false } |
locator | { "name": "myArg", "type": "locator" } | { "value": { "css": ".myClass" }, "type": "locator" } |
element reference | not supported | { "type": "elementReference", "value": "myElement" } |
page object type | { "name": "myArg", "type": "pageObject" } | { "value": "my/page/object", "type": "pageObject" } |
root page object type | { "name": "myArg", "type": "rootPageObject" } | { "value": "my/page/object", "type": "rootPageObject" } |
frame element | { "name": "myArg", "type": "frame" } | same as element reference |
function | not supported | { "type": "function", "predicate": [...] } |
Compose Container Invocation
A pageObject
type parameter can be used to invoke a container method inside a compose statement.
{
"elements": [
{
"name": "containerElement",
"type": "container"
}
],
"methods": [
{
"name": "composeContainerHardcoded",
"compose": [
{
"element": "containerElement",
"args": [
{
"type": "pageObject",
"value": "utam-tests/pageObjects/myPageObject"
}
],
"returnType": "utam-tests/pageObjects/myPageObject"
}
]
},
{
"name": "composeContainer",
"compose": [
{
"element": "containerElement",
"args": [
{
"type": "pageObject",
"name": "pageObjectCtor"
}
]
}
]
}
]
}
Generated JavaScript code:
// declaration
composeContainerHardcoded(): Promise<unknown>;
composeContainer<T extends _UtamBasePageObject>(pageObjectCtor: _PageObjectCtor<T>): Promise<unknown>;
// implementation
import _MyPageObject from 'utam-tests/pageObjects/myPageObject';
async composeContainerHardcoded() {
const _result0 = await this.__getContainer(_MyPageObject);
return _result0;
}
async composeContainer(pageObjectCtor) {
const _result0 = await this.__getContainer(pageObjectCtor);
return _result0;
}
Compose Entering Frame
Frame and Root Page Object type parameters can be used to compose entering a frame:
{
"elements": [
{
"name": "frameElement",
"type": "frame",
"selector": {
"css": "#frame"
}
}
],
"methods": [
{
"name": "composeEnterFrameHardcoded",
"compose": [
{
"element": "document",
"returnType": "utam-tests/pageObjects/frameArea",
"apply": "enterFrameAndLoad",
"args": [
{
"type": "elementReference",
"value": "frameElement"
},
{
"type": "rootPageObject",
"value": "utam-tests/pageObjects/frameArea"
}
]
}
]
},
{
"name": "composeEnterFrame",
"compose": [
{
"element": "document",
"returnType": "rootPageObject",
"apply": "enterFrameAndLoad",
"args": [
{
"type": "frame",
"name": "frameElementParameter"
},
{
"type": "rootPageObject",
"name": "pageObjectCtor"
}
]
}
]
}
]
}
Generated JavaScript code:
// declaration
import _FrameArea from 'utam-tests/pageObjects/frameArea';
composeEnterFrameHardcoded(): Promise<_FrameArea>;
composeEnterFrame<T extends _UtamBaseRootPageObject>(frameElementParameter: _FrameUtamElement, pageObjectCtor: _PageObjectCtor<T>): Promise<T>;
// implementation
import _FrameArea from 'utam-tests/pageObjects/frameArea';
async composeEnterFrameHardcoded() {
const _statement0 = await this.getDocument();
const _result0 = await _statement0.enterFrameAndLoad(await this.__getFrameElement(), _FrameArea);
return _result0;
}
async composeEnterFrame(frameElementParameter, pageObjectCtor) {
const _statement0 = await this.getDocument();
const _result0 = await _statement0.enterFrameAndLoad(frameElementParameter, pageObjectCtor);
return _result0;
}
Element Reference
The elementReference
type in an argument allows you to reference an existing element as a parameter value. This example uses an elementReference
to a targetElement
element.
{
"elements": [
{
"name": "sourceElement",
"selector": {
"css": ".source"
},
"type": ["draggable"]
},
{
"name": "targetElement",
"selector": {
"css": ".source"
}
}
],
"methods": [
{
"name": "composeDragAndDrop",
"compose": [
{
"element": "sourceElement",
"apply": "dragAndDrop",
"args": [
{
"type": "elementReference",
"value": "targetElement"
}
]
}
]
}
]
}
Generated JavaScript code:
// declaration
composeDragAndDrop(): Promise<void>;
// implementation
async composeDragAndDrop() {
const _statement0 = await this.__getSourceElement();
await _statement0.dragAndDrop(await this.__getTargetElement());
}
If a referenced element has a selector or filter arguments, those values are automatically inferred and added to the method declaration.
{
"name": "target",
"selector": {
"css": "foo:nth-child(%d)",
"args": [
{
"name": "index",
"type": "number"
}
]
}
}
When the element is referenced in a compose statement, this page object hard codes the value of 1 for an index.
{
"element": "source",
"apply": "dragAndDrop",
"args": [
{
"value": "target",
"type": "elementReference",
"args": [
{
"value": 1
}
]
}
]
}
Explicit Waits
We define an explicit wait by setting the apply
value to waitFor
, which is a public method that exercises a fluent wait for an element.
The wait wraps one or more functions and repeatedly invokes them within a timeout until they return a truthy value (boolean true
or any value that isn't null
or undefined
).
The function invoked inside waitFor
is defined in the args
array as a parameter for a method used in a compose statement:
type
is set tofunction
predicate
is an array of compose statements that is invoked to test for the wait condition.
The predicate
value is a list of compose statements that will be executed until truthy value is returned (no error, not null and not false).
{
"apply": "waitFor",
"args": [
{
"type": "function",
"predicate": [
{
"element": "foo",
"apply": "click"
}
]
}
]
}
Return type inside predicate
If a compose statement is a "waitFor"
action or an explicit wait, the return type is the return type of the last predicate statement. Similarly to a regular statement, the return type can be inferred, void, or set explicitly. Here are some examples.
- return type can be inferred because it's an element getter invocation, so this method returns the custom element type
{
"elements": [
{
"name": "custom",
"selector": {
"css": "my-custom-element"
},
"type": "my/custom/element"
}
],
"methods": [
{
"name": "returnCustomElement",
"compose": [
{
"apply": "waitFor",
"args": [
{
"type": "function",
"predicate": [
{
"element": "custom"
}
]
}
]
}
]
}
]
}
- return type is set explicitly, so this method returns a list or array of strings as specified in the last statement of the predicate
{
"elements": [
{
"name": "custom",
"selector": {
"css": "my-custom-element"
},
"type": "my/custom/element"
}
],
"methods": [
{
"name": "returnCustomElement",
"compose": [
{
"apply": "waitFor",
"args": [
{
"type": "function",
"predicate": [
{
"element": "custom"
},
{
"chain": true,
"apply": "customElementPublicMethod",
"returnType": "string",
"returnAll": true
}
]
}
]
}
]
}
]
}
Important: setting a
returnType
outside a predicate isn't allowed. This page object throws an error:
{
"methods": [
{
"name": "clickAndReturn",
"compose": [
{
"apply": "waitFor",
"args": [
{
"type": "function",
"predicate": [
{
"apply": "getString"
}
]
}
],
"returnType": "string"
}
]
}
]
}
beforeLoad
The beforeLoad
array sets the criteria to be satisfied before the load
method completes. If you don't specify a beforeLoad
array, the load
method finds a root element for a regular page object, or waits for the root element to be present for a root page object), by default.
Note: Statements inside the
beforeLoad
array can reference onlyroot
ordocument
elements because other elements are not loaded yet.
This ApplicationHome
page object waits for the DOM to finish rendering before loading is complete and then waits for the root and indicator elements:
{
"selector": {
"css": "application-home"
},
"root": true,
"beforeLoad": [
{
"element": "document",
"apply": "waitForDocumentReady"
},
{
"apply": "waitFor",
"args": [
{
"type": "function",
"predicate": [
{
"element": "root",
"apply": "isPresent"
},
{
"element": "root",
"apply": "containsElement",
"args": [
{
"type": "locator",
"value": {
"css": ".indicator"
}
}
]
}
]
}
]
}
]
}
Imperative Extensions
Imperative extensions enable you to create reusable utility code to address complex use cases that aren't supported in UTAM's JSON grammar. For example, you can't use the JSON grammar to navigate a data table where each column shows data from a different type of component with different markup.
Note: Imperative extensions are powerful because you can use the full capabilities of a programming language. However, we strongly discourage their usage except as a last resort because they violate the declarative nature of UTAM. If you use an imperative extension, you must implement and maintain it in each programming language that you use with UTAM.
Declare imperative utility code in a compose
statement. A page object can reference multiple utilities.
{
"methods": [
{
"name": "myMethod",
"compose": [
{
"applyExternal": {
"type": "utam-lst/utils/lst/relatedListContainerUtils",
"invoke": "cardUtility1",
"args": [
{
"name": "stringArg",
"type": "string"
}
]
}
},
{
"applyExternal": {
"type": "utam-lst/utils/lst/relatedListContainerUtils",
"invoke": "cardUtility2",
"args": [
{
"name": "booleanArg",
"type": "boolean"
}
]
}
}
]
}
]
}
An imperative extension in a compose
statement has these properties:
element
(Optional.) String. A reference to the element passed as the first parameter to the utility code. The default value isself
so if no element is set, we pass the page object itself. You can omit this property because you can access the elements from the page object reference passed to the utility code.applyExternal
(Required) Object. Declares an imperative extension and has these properties:type
(Required) String. A reference to the executable code to import: either a Java class or a JavaScript ES module.invoke
(Required) String. A reference to the method name to call: a named function in JavaScript or a static method in Java.args
(Optional) Array. If the code needs parameters, provide them in an array. Each element of the array has these properties:name
(Required) String. A parameter name that is unique in the scope of theargs
array.type
(Required) String. A parameter type, which is a primitive type, locator or a function.
For examples of Java and JavaScript code implementations, see Imperative Extensions.
Platform Context
In a hybrid mobile application, some pages are WebView and some pages are native. WebView allows you to display web content inside a native app. A WebView page leverages the device's browser engine to render HTML and process JavaScript locally. A native page is built with a platform SDK such as iOS or Android, using tools and languages for the specific platform.
Appium is an open-source tool for automating native, mobile web, and hybrid applications. Appium allows you to write tests against multiple platforms, such as iOS and Android, using the same API. For a hybrid mobile application test using Appium, the driver needs to enter the appropriate Appium context: WebView context for a WebView page or native context for a native page. For more details, see appium.io: Appium Context.
The platform
field at the root of a page object defines the page context type. The value can be:
-
web
for a WebView page. This is the default value. -
native
for a native page on iOS or Android platforms.
The UTAM framework uses the platform
property to set WebView or native context at the page object bootstrap stage. Then, a test can interact appropriately with elements on the page.
Here's the JSON for a WebView page object (platform
defaults to web
):
{
"selector": {
"css": "div#main"
},
"elements": []
}
Here's the JSON for a native page object on Android:
{
"platform": "native",
"selector": {
// native selector here
},
"elements": []
}