Element Types
Elements can have different types.
Basic Elements
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 Filters. -
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 Elements
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 Elements
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 Elements
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.