Resource library

QA How-To

Selenium Test Web Components Slots Tutorial: Test Web Component Slots with Selenium

Use this selenium test web components slots tutorial to verify named, default, fallback, reassigned, and dynamic slot content with runnable Java tests.

19 min read | 2,398 words

TL;DR

Locate the custom element, enter its open shadow root with getShadowRoot(), and assert both the light DOM nodes and the slot elements. Use small JavaScript queries only for slot-specific DOM APIs such as assignedSlot and assignedElements(), then wait explicitly for dynamic composition changes.

Key Takeaways

  • Treat light DOM content and the shadow DOM slot element as two separate test surfaces.
  • Use Selenium's getShadowRoot API instead of JavaScript for ordinary open shadow roots.
  • Verify slot assignment with assignedSlot and assignedElements when rendered composition matters.
  • Test default, named, fallback, reassigned, and removed content as distinct behaviors.
  • Wait on slot state or application state instead of adding fixed sleeps.
  • Keep component tests focused on the public contract and a small number of composition assertions.

A selenium test web components slots tutorial must test more than text inside a custom element. You need to prove that light DOM children are assigned to the correct default or named slot, fallback content appears when no node is assigned, and dynamic changes update the composed UI.

This tutorial builds those checks in Java with Selenium 4, JUnit 5, and a local HTML fixture. If shadow roots are new to you, read the complete Selenium Shadow DOM testing guide first. It explains host elements, open and closed roots, browser support, and locator boundaries.

You will use Selenium's native getShadowRoot() API for ordinary shadow-tree traversal. You will use JavaScript only where the Web Components platform exposes slot-specific properties that WebDriver does not model directly, such as assignedSlot and assignedElements().

What You Will Build

You will create a small <user-card> Web Component and a runnable test suite that verifies:

  • The heading is projected into a named title slot.
  • Unnamed content is projected into the default slot.
  • Fallback text appears when the default slot has no assigned nodes.
  • A light DOM child can move from one named slot to another.
  • Content added after a timer becomes visible without a fixed sleep.

The result separates three useful assertions: light DOM ownership, shadow DOM structure, and browser-computed slot assignment. That separation makes failures easier to diagnose.

Prerequisites

Use Java 17 or newer, Maven 3.9 or newer, Selenium 4.18 or newer, JUnit Jupiter 5.10 or newer, and a current Chrome or Chromium browser. The examples use Selenium Manager, included with Selenium, so you do not need to download a driver manually.

Verify your tools:

java -version
mvn -version

Create an empty Maven project with this layout:

slot-testing/
├── pom.xml
├── src/
│   ├── main/resources/user-card.html
│   └── test/java/example/UserCardSlotsTest.java

Run all commands from slot-testing. Chrome must be available on the machine running the tests. In CI, use the browser image or installation approved by your team.

Step 1: Create the Selenium Test Project

Add this complete pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>slot-testing</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <selenium.version>4.27.0</selenium.version>
    <junit.version>5.11.4</junit.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.2</version>
        <configuration>
          <useModulePath>false</useModulePath>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

These versions are concrete and runnable. A later compatible Selenium or JUnit release should also work, but update deliberately through your dependency process.

Verify: run mvn test -DskipTests. Maven should finish with BUILD SUCCESS after resolving dependencies. No test runs yet.

Step 2: Build a Web Component with Default and Named Slots

Create src/main/resources/user-card.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>User card slot fixture</title>
</head>
<body>
  <user-card id="ada-card">
    <span id="card-title" slot="title">Ada Lovelace</span>
    <p id="card-bio">First computer programmer</p>
    <button id="card-action" slot="actions">View profile</button>
  </user-card>

  <user-card id="empty-card">
    <span slot="title">Grace Hopper</span>
  </user-card>

  <script>
    class UserCard extends HTMLElement {
      constructor() {
        super();
        const root = this.attachShadow({ mode: 'open' });
        root.innerHTML = `
          <style>
            article { border: 1px solid #777; padding: 12px; }
          </style>
          <article>
            <h2><slot name="title">Unknown user</slot></h2>
            <div class="bio"><slot>No biography supplied</slot></div>
            <footer><slot name="actions"></slot></footer>
          </article>`;
      }
    }
    customElements.define('user-card', UserCard);
  </script>
</body>
</html>

The <span slot="title"> remains a light DOM child of user-card. The shadow tree contains a <slot name="title"> insertion point. The browser's composed tree renders the span inside the heading, but the node is not physically moved into the shadow root.

Verify: open the file in Chrome. The first card should display Ada Lovelace, First computer programmer, and View profile. The second should display Grace Hopper and the fallback No biography supplied.

Step 3: Open the Fixture and Enter the Shadow Root

Create src/test/java/example/UserCardSlotsTest.java with the shared test setup:

package example;

import static org.junit.jupiter.api.Assertions.*;

import java.net.URL;
import java.time.Duration;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;

class UserCardSlotsTest {
  private WebDriver driver;
  private WebDriverWait wait;
  private JavascriptExecutor js;

  @BeforeEach
  void openFixture() {
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--headless=new", "--window-size=1280,800");
    driver = new ChromeDriver(options);
    wait = new WebDriverWait(driver, Duration.ofSeconds(5));
    js = (JavascriptExecutor) driver;

    URL fixture = getClass().getClassLoader().getResource("user-card.html");
    assertNotNull(fixture, "Fixture must be present on the test classpath");
    driver.get(fixture.toExternalForm());
    wait.until(d -> (Boolean) ((JavascriptExecutor) d).executeScript(
        "return customElements.get('user-card') !== undefined"));
  }

  @AfterEach
  void closeBrowser() {
    if (driver != null) {
      driver.quit();
    }
  }

  private WebElement host(String id) {
    return driver.findElement(By.id(id));
  }

  private SearchContext shadowRoot(String hostId) {
    return host(hostId).getShadowRoot();
  }
}

getShadowRoot() returns a SearchContext. Search inside that context for shadow descendants. Do not try driver.findElement(By.cssSelector("user-card slot")); normal CSS selectors do not cross a shadow boundary.

Verify: add an empty @Test void opensPage() { assertEquals("User card slot fixture", driver.getTitle()); } method temporarily, then run mvn test. Expect one passing test. Remove the temporary method before continuing.

Step 4: Test Named Slot Assignment

Add this method inside UserCardSlotsTest:

@Test
void assignsHeadingToNamedTitleSlot() {
  WebElement card = host("ada-card");
  WebElement title = card.findElement(By.id("card-title"));
  SearchContext shadow = card.getShadowRoot();
  WebElement titleSlot = shadow.findElement(By.cssSelector("slot[name='title']"));

  assertEquals("title", title.getAttribute("slot"));
  assertEquals("Ada Lovelace", titleSlot.getText());

  WebElement assignedSlot = (WebElement) js.executeScript(
      "return arguments[0].assignedSlot", title);
  assertNotNull(assignedSlot);
  assertEquals("title", assignedSlot.getAttribute("name"));
}

The first assertion checks the author's input. The second checks visible composition. The assignedSlot assertion checks the browser's actual assignment result. This layered approach catches a wrongly named slot even when duplicated text elsewhere makes a display-only assertion pass.

Selenium can return a DOM element from executeScript as WebElement. Keep the script narrow and pass elements through arguments; do not construct selectors from untrusted text.

Verify: run mvn -Dtest=UserCardSlotsTest#assignsHeadingToNamedTitleSlot test. Expect one test and zero failures. Change slot="title" to slot="wrong" in the fixture and confirm the test fails, then restore it.

Step 5: Test Default Slots and Fallback Content

A slot without a name is the default slot. Children without a slot attribute can be assigned to it. Test assigned and unassigned states separately:

@Test
void assignsUnslottedBiographyToDefaultSlot() {
  WebElement card = host("ada-card");
  WebElement bio = card.findElement(By.id("card-bio"));
  WebElement defaultSlot = card.getShadowRoot()
      .findElement(By.cssSelector("slot:not([name])"));

  assertNull(bio.getAttribute("slot"));
  assertEquals("First computer programmer", defaultSlot.getText());

  Number assignedCount = (Number) js.executeScript(
      "return arguments[0].assignedElements().length", defaultSlot);
  assertEquals(1L, assignedCount.longValue());
}

@Test
void showsFallbackWhenDefaultSlotHasNoAssignedElements() {
  WebElement defaultSlot = shadowRoot("empty-card")
      .findElement(By.cssSelector("slot:not([name])"));

  Number assignedCount = (Number) js.executeScript(
      "return arguments[0].assignedElements().length", defaultSlot);
  assertEquals(0L, assignedCount.longValue());
  assertEquals("No biography supplied", defaultSlot.getText());
}

assignedElements() ignores text nodes, which is appropriate when your component contract expects elements. Use assignedNodes() when raw text nodes are valid input. Fallback children belong to the slot element, but they are not returned as assigned nodes.

Question Best assertion Why
Did the consumer supply the right marker? getAttribute("slot") Checks light DOM input
Did the user see the expected text? slot.getText() Checks rendered result
Which slot received the node? assignedSlot Checks computed assignment
Is fallback active? assignedElements().length == 0 plus text Distinguishes fallback from projection

Verify: run mvn test. Expect three passing tests. Remove the biography from ada-card; the default assignment test should fail while the fallback test remains green.

Step 6: Test Reassignment Between Named Slots

Slot behavior can change without replacing a node. Add a second named slot to the fixture's <footer> so it reads:

<footer>
  <slot name="actions"></slot>
  <slot name="secondary-actions"></slot>
</footer>

Then add this test:

@Test
void reassignsActionWhenSlotAttributeChanges() {
  WebElement card = host("ada-card");
  WebElement action = card.findElement(By.id("card-action"));
  SearchContext shadow = card.getShadowRoot();
  WebElement primary = shadow.findElement(By.cssSelector("slot[name='actions']"));
  WebElement secondary = shadow.findElement(
      By.cssSelector("slot[name='secondary-actions']"));

  js.executeScript("arguments[0].setAttribute('slot', 'secondary-actions')", action);

  wait.until(d -> "secondary-actions".equals(js.executeScript(
      "return arguments[0].assignedSlot && arguments[0].assignedSlot.name", action)));

  assertEquals(0L, ((Number) js.executeScript(
      "return arguments[0].assignedElements().length", primary)).longValue());
  assertEquals(1L, ((Number) js.executeScript(
      "return arguments[0].assignedElements().length", secondary)).longValue());
  assertEquals("View profile", secondary.getText());
}

setAttribute changes the content attribute and triggers the browser's slot assignment algorithm. The explicit wait observes the computed destination, not merely the attribute mutation. This matters when component code performs additional asynchronous work around the change.

Verify: run the single method. Expect it to pass. Replace secondary-actions in the wait with a nonexistent name and confirm that the wait times out with a useful failure location. Restore the correct value.

Step 7: Wait for Dynamically Slotted Content

Add a button after the two cards in the fixture:

<button id="load-action">Load action</button>
<script>
  document.querySelector('#load-action').addEventListener('click', () => {
    setTimeout(() => {
      const button = document.createElement('button');
      button.id = 'dynamic-action';
      button.slot = 'actions';
      button.textContent = 'Send message';
      document.querySelector('#empty-card').append(button);
    }, 150);
  });
</script>

Place this script after customElements.define(...), or combine it into the existing script after the definition. Now add the test:

@Test
void waitsForDynamicNodeToJoinNamedSlot() {
  WebElement emptyCard = host("empty-card");
  WebElement actionsSlot = emptyCard.getShadowRoot()
      .findElement(By.cssSelector("slot[name='actions']"));

  driver.findElement(By.id("load-action")).click();

  wait.until(d -> ((Number) js.executeScript(
      "return arguments[0].assignedElements().length", actionsSlot)).intValue() == 1);

  WebElement dynamicAction = emptyCard.findElement(By.id("dynamic-action"));
  assertEquals("Send message", dynamicAction.getText());
  assertEquals("actions", js.executeScript(
      "return arguments[0].assignedSlot.name", dynamicAction));
}

Do not use Thread.sleep(500). A fixed pause makes a fast test unnecessarily slow and can still fail on a loaded worker. Wait for the state that matters: one element assigned to the named slot. For more patterns, see waiting for dynamic Shadow DOM elements in Selenium.

Verify: run mvn test. Expect all five tests to pass. Increase the fixture delay to 1,000 ms; the test should still pass within the five-second explicit wait.

Step 8: Make the Suite Maintainable

Extract slot queries only when several tests share them. Keep the host ID explicit because it tells the reader which component instance failed. A small helper for assigned elements is useful:

@SuppressWarnings("unchecked")
private List<WebElement> assignedElements(WebElement slot) {
  return (List<WebElement>) js.executeScript(
      "return arguments[0].assignedElements()", slot);
}

Then an assertion can read assertEquals(1, assignedElements(actionsSlot).size()). Selenium converts a returned JavaScript array of elements into a Java List<WebElement>.

Keep end-to-end slot tests at the public contract level. Assert the slot name, meaningful content, accessibility, and the behavior triggered by the projected control. Avoid checking every wrapper or CSS class in the private shadow tree. Component-level browser tests can cover detailed rendering.

If a component contains shadow hosts inside other shadow roots, traverse one boundary at a time. The nested shadow roots Java tutorial shows a reusable approach. Python teams can use the same test model with the Selenium Shadow DOM Python locator examples.

Verify: replace the count script in one test with assignedElements(slot).size(), run mvn test, and confirm behavior is unchanged. Your final suite should report five passing tests. Review failure messages before committing the helper. A useful failure identifies the component host, expected slot, actual slot, and assigned content without dumping the entire page. Keep helper methods stateless so parallel test execution cannot leak one component instance into another. If the application re-renders the host, locate the host and slot again instead of retaining stale element references. Add one behavior assertion for each projected control, such as clicking the action and checking the resulting profile view. This proves composition is not only visually correct but interactive. Finally, run the suite in the same browser families your product supports because slot behavior is standardized while layout, focus, and WebDriver text details can still expose integration differences. In CI, save the page source, browser console log, and a screenshot when a test fails. The page source shows light DOM input, while the screenshot shows the composed result. Neither artifact alone explains both sides of a slot failure.

Treat slot names as part of the component public API. If the application changes the actions slot to primary-actions, update the component contract and its consumers together. A compatibility test that mounts the component with production-like light DOM catches a mismatch earlier than a broad end-to-end journey.

Also decide whether ordering belongs to the contract. The assignedElements() method returns nodes in slot assignment order, which normally follows the order of slotable children in the host. Assert order only when users or keyboard navigation depend on it. Otherwise compare the relevant IDs or text without making the test sensitive to harmless markup reordering.

Troubleshooting

Problem: NoSuchShadowRootException occurs -> Wait until the custom element is defined and its constructor has attached an open root. Confirm the component uses mode: 'open'. Selenium cannot access a closed root through getShadowRoot().

Problem: a CSS selector cannot find a slot -> Start the lookup from host.getShadowRoot(). A selector issued from driver or the light DOM host does not pierce the shadow boundary.

Problem: slot.getText() is empty even though assigned content exists -> First check assignedElements() and inspect the assigned element's text. Browser and driver text computation can differ for hidden or specially styled content, so assert the public visible element when that is the real contract.

Problem: fallback text and assigned text appear confusing -> Check assignedElements().length. A zero count means fallback can render. A nonzero count means assigned nodes replace fallback in the composed tree.

Problem: a dynamic test is flaky -> Wait for assignedSlot, assignedElements().length, or a visible business state. Do not wait only for the host, because the host often exists before its content is ready.

Problem: ClassCastException occurs after JavaScript execution -> JavaScript numbers return as a Number, not a guaranteed Long or Integer. Cast to Number, then call longValue() or intValue().

Selenium Test Web Components Slots Tutorial Best Practices and Common Mistakes

  • Do test both the light DOM input and computed slot assignment when slot correctness is important.
  • Do select slots by their name attribute. The order of slot elements is usually an implementation detail.
  • Do verify fallback with an assignment count and user-visible text.
  • Do keep JavaScript small and limited to platform APIs missing from WebDriver.
  • Do wait for a meaningful composed state after asynchronous changes.
  • Do not flatten every slot automatically. assignedElements({flatten: true}) changes the question by following nested slot forwarding. Use it only when the public contract requires flattened results.
  • Do not expect XPath or CSS from the document root to cross a shadow boundary.
  • Do not assert only innerHTML; it does not represent the composed tree and produces brittle tests.
  • Do not automate closed shadow roots by patching production code. Test them through public behavior or a supported component test seam.

Use a three-layer failure model when a test breaks. First inspect the light DOM child and its slot attribute. Next inspect the matching slot element inside the open shadow root. Finally inspect assignedSlot, assigned elements, and visible behavior. If the first layer is wrong, fix the consumer markup. If the second is wrong, fix the component template. If both are correct but composition or behavior is wrong, investigate timing, conditional rendering, styling, or browser integration. This sequence keeps diagnosis targeted and prevents a large JavaScript probe from hiding the actual boundary that failed.

Where To Go Next

You now have a focused slot suite. Use the Selenium Shadow DOM testing complete guide to place these checks within a broader component strategy.

Continue with these hands-on guides:

Also test accessibility at the rendered boundary. A projected button should keep its accessible name and keyboard behavior. Slot assignment passing does not guarantee that the completed component is usable.

Interview Questions and Answers

Q: Where does a slotted element live in the DOM?

It remains a child in the host's light DOM. The browser assigns it to a matching <slot> for the composed rendering tree. Selenium can locate the light DOM element from the host and the slot element from the shadow root.

Q: How do you enter an open shadow root in Selenium Java?

Locate the shadow host and call getShadowRoot(). The returned SearchContext supports findElement and findElements inside that root. Repeat the process at each nested shadow boundary.

Q: How do you prove that a node was assigned to the correct slot?

Read the node's assignedSlot property with a narrow JavaScript call, then assert the returned slot's name. Combine that with visible text or behavior so the test covers both structure and user outcome.

Q: What is the difference between assignedNodes and assignedElements?

assignedNodes() includes element and text nodes. assignedElements() returns only elements. Choose based on whether raw text is valid component input.

Q: How do you test slot fallback content?

Assert that assignedElements() or assignedNodes() is empty, then assert the fallback is visible. The count distinguishes genuine fallback from identical text supplied by a consumer.

Q: Why should Selenium tests avoid fixed sleeps for slot changes?

Slot assignment may occur quickly or after asynchronous component work. A sleep wastes time when work is fast and fails when work is slower than expected. An explicit wait against assignedSlot or assigned element count tracks the actual condition.

Q: Can Selenium access a closed shadow root?

Not through the standard shadow root API. Test closed-root components through their public behavior, accessible output, and supported test interfaces instead of breaking encapsulation.

Conclusion

Reliable Web Component slot tests distinguish the light DOM node, the shadow DOM insertion point, and the browser's composed result. Use getShadowRoot() for open shadow traversal, slot DOM APIs for exact assignment, and explicit waits for dynamic changes.

Run the finished suite, deliberately break each slot name once, and confirm the matching test fails for the right reason. That small mutation check proves your assertions detect composition regressions instead of merely finding repeated text.

Interview Questions and Answers

Where does a slotted element live in the DOM?

It remains in the shadow host's light DOM. The browser associates it with a slot for the composed rendering tree, but does not move it into the shadow DOM. This is why Selenium locates the child from the host and the slot from the shadow root.

How do you access an open shadow root with Selenium Java?

Locate the shadow host and call getShadowRoot(). It returns a SearchContext used to locate descendants inside that root. For nested components, repeat the operation one shadow boundary at a time.

How would you verify exact named slot assignment?

I would first assert the child's slot attribute, then read its assignedSlot property with executeScript and assert the returned slot name. I would also check visible content or behavior so the test covers the rendered outcome.

What is the difference between assignedNodes() and assignedElements()?

assignedNodes() returns assigned elements and text nodes. assignedElements() filters the result to elements. I choose based on whether the component accepts raw text as meaningful input.

How do you verify fallback content in a slot?

I assert that the slot has no assigned nodes or elements and then assert its fallback text or behavior. Checking both prevents a supplied node with identical text from causing a false positive.

How do you synchronize a test with dynamic slot assignment?

I use an explicit wait against the state that proves composition completed, such as assignedSlot.name or assignedElements().length. I avoid fixed sleeps because they do not track actual readiness and become flaky under variable load.

Why can a document-level CSS selector not find a slot inside a shadow root?

A shadow boundary creates a separate selector scope. I must locate the host, enter its open shadow root, and issue the slot selector from the returned SearchContext. Standard CSS and XPath searches do not pierce the boundary.

Frequently Asked Questions

How do I test Web Component slots with Selenium?

Locate the custom-element host, call getShadowRoot(), and find the relevant slot inside the returned SearchContext. Assert visible content, then use assignedSlot or assignedElements() through executeScript when you need to prove the browser's exact assignment.

Can Selenium locate a slotted element inside a shadow root?

The slotted element remains in the host's light DOM, so locate it from the host or document. The slot insertion point lives inside the shadow root and must be located from the shadow SearchContext.

How do I test a named slot?

Assert the light DOM child's slot attribute, find the shadow slot by its name, and check the child's assignedSlot property. Add a visible text or behavior assertion to cover the user-facing result.

How do I know whether slot fallback content is active?

Check that assignedElements() or assignedNodes() returns an empty collection, then assert the fallback content is visible. This prevents identical consumer content from creating a false positive.

Should I use JavaScript to enter a shadow root in Selenium?

Use Selenium's native getShadowRoot() API for open roots. Reserve JavaScript for slot-specific platform properties such as assignedSlot and assignedElements(), which WebDriver does not expose directly.

How do I wait for dynamically assigned slot content?

Use WebDriverWait to poll the assigned element count, the node's assignedSlot name, or a visible business outcome. Avoid Thread.sleep because it is both slower and less reliable.

Can Selenium test slots in a closed shadow root?

Selenium cannot enter a closed shadow root through the standard API. Test its public behavior and accessibility, or use a component-owned test seam if the development team explicitly provides one.

Related Guides