Working with slots
A slot is a placeholder inside a web component that you can fill with your own markup, which lets you create separate DOM trees and present them together. See MDN: slots documentation.
Slot in a Web Component
Let's look at an example of how to use slots in a page object. Here's a modal with account fields. We're using the Salesforce application as an example, but we could use any application that has an account object with certain fields.
As you can see from the screenshot, the <records-record-layout-item>
component has an element <slot name="inputField"></slot>
that can contain other components like input, lookup, or picklist depending on the account field type.
For the field that we selected for inspection, the content of the slot is a <records-record-layout-lookup slot="inputField">...</records-record-layout-lookup>
element that has other nested HTML elements.
Slot in a UTAM page object
To test the content of the <records-record-layout-item>
component, our page object needs an "inputFieldContainer"
container element because the item can contain any component inside the slot depending on the setup:
{
"elements": [
{
"type": "container",
"name": "inputFieldContainer",
"public": true,
"selector": {
"css": "[slot='inputField']"
}
}
]
}
The first thing to notice is that the content of the slot isn't inside the shadow root, only the slot itself is.
If a web developer puts something inside a <slot>
, it becomes the fallback or “default” content. The browser shows it if there’s no corresponding filler in light DOM. Even though our component has a shadow root, the container element isn't crossing the shadow boundary and isn't inside the "shadow"
element.
Locating slot and slot content
If a page object element points to a slot itself (for example with "slot[name='inputField']"
), its internal elements can only be found using assignedNodes(), which isn't supported in UI automation tools. All we can do with such an element is to call basic actions, such as checking its presence or getting its attributes and text.
To point to another component inside the slot, the selector of our container element ("inputFieldContainer"
) should point to the content of the slot, not to the slot itself: "[slot='inputField']"
.
Locating content of default slots
To make a UI component more generic, a developer can sometimes add a default slot (a slot without a name).
<div class="slots">
<slot name="inputField"></slot>
<!-- default slot after named slot -->
<slot></slot>
</div>
At run time, the content of the slot can be anything so the page object can handle it in one of the following ways:
- if the slot is the first element of its parent, use the default container
If you try to access the content of the first slot inside its parent, use the default selector (do not add a "selector"
property to the JSON).
For example for the given HTML:
<template>
<!-- first slot is default slot -->
<slot></slot>
</template>
The following container element can access the content.
{
"elements": [
{
"type": "container",
"name": "firstSlotContent",
"public": true
// default selector is ":scope > *:first-child"
}
]
}
- add a custom element of the hardcoded type (not recommended) This approach is not a good option because you would have to add a method for every possible element. However, it might be a better option than hardcoding a selector in every test.
{
"elements": [
{
"type": "my/component",
"name": "myComponentInsideSlot",
"public": true,
"selector": {
"css": "my-component"
}
}
]
}
-
locate with hardcoded css parameter passed at run time (not recommended) A parameter can expose a hardcoded selector to the test; for example:
field.getRuntimeContent(MyComponent, "my-component")
. This approach violates encapsulation, but sometimes it's the only option.Because we do not know CSS selector to locate content, it has to be added as a parameter.
{
"elements": [
{
"type": "container",
"name": "runtimeContent",
"public": true,
"selector": {
"css": "%s",
"args": {
"name": "hardcodedSelector",
"type": "string"
}
}
}
]
}
This parameter exposes a hardcoded selector to a UI test (for our example test code would be: field.getRuntimeContent(MyComponent, "my-component")
.
This approach violates encapsulation, but can be the only available option for a page object author.
Using a slot in a UI test
For our example, the UI test uses field
, which is an instance of our page object with a slot:
- Java
RecordLayoutLookup lookup = field.getInputFieldContainer(RecordLayoutLookup.class);
lookup.doSomething();
- JavaScript
const lookup = await field.getInputFieldContainer(RecordLayoutLookup);
await lookup.doSomething();
The test invokes the container method with the type of the content to be loaded ("RecordLayoutLookup"
) and then interacts with the returned content.
Summary
- A slot inside a web component translates into a container element inside the UTAM page object
- A container's selector should always point to the slot content, never to the slot itself
- A container element is outside the shadow root of its immediate parent