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.

Inspected slot

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 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"
        }
    ]
}
{
    "elements": [
        {
            "type": "my/component",
            "name": "myComponentInsideSlot",
            "public": true,
            "selector": {
                "css": "my-component"
            }
        }
    ]
}
{
    "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:

RecordLayoutLookup lookup = field.getInputFieldContainer(RecordLayoutLookup.class);
lookup.doSomething();
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