Search code examples
javascriptjestjsweb-componentshadow-domnative-web-component

How to test slotted elements in web components with jest in javascript (no Framework)


I want to test the content of a slot in one of my custom components. If I use my component in an html-file and open it in an browser, everything works as intended. However if I want to automate my test with jest, it fails. Below is an minimal working example with the output form jest:


placeholder.js:

const template = document.createElement("template");
template.innerHTML = `
    <p>
        <slot></slot>
    </p>
`;


class Placeholder extends HTMLElement {
    constructor() {
        super();

        this.attachShadow({ mode: "open" });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    get name() {
        return this.shadowRoot.querySelector("slot").innerText;
    }
}

window.customElements.define("place-holder", Placeholder);

export default Placeholder;

placeholder.test.js:

import Placeholder from "../src/placeholder.js";

describe("name is 'Lorem Ipsum'", () => {
    let ph;
    
    beforeAll(() => {
        ph = new Placeholder();
        const textNode = document.createTextNode("Lorem Ipsum");
        ph.appendChild(textNode);
    });

    test("if the name is 'Lorem Ipsum'", () => {
        expect(ph.name).toBe("Lorem Ipsum");
    });
});

output:

name is 'Lorem Ipsum' › if the name is 'Lorem Ipsum'

expect(received).toBe(expected) // Object.is equality

Expected: "Lorem Ipsum"
Received: undefined

  11 |
  12 |     test("if the name is 'Lorem Ipsum'", () => {
> 13 |         expect(ph.name).toBe("Lorem Ipsum");
     |                         ^
  14 |     });
  15 | });

  at Object.<anonymous> (test/placeholder.test.js:13:25)
  at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)
  at runJest (node_modules/@jest/core/build/runJest.js:387:19)
  at _run10000 (node_modules/@jest/core/build/cli/index.js:408:7)
  at runCLI (node_modules/@jest/core/build/cli/index.js:261:3)

As you can see jest somehow fails to get the slotted text and returns undefined. How can I solve this problem?


Solution

  • The text node will not be a part of the <slot> element's internals. It is only a wrapper to the text node. To get the nodes that are placed inside of the slot you have to use HTMLSlotElement.assignedNodes() method.

    The assignedNodes() method of the HTMLSlotElement interface returns a sequence of the nodes assigned to this slot.

    With this you get an array of nodes that reside in the slot. The added text node will be in this array.

    I've modified your name getter to get the first node out of the assigned nodes array and return the textContent value of the node.

    const template = document.createElement("template");
    template.innerHTML = `
      <p>
        <slot></slot>
      </p>
    `;
    
    
    class Placeholder extends HTMLElement {
      constructor() {
        super();
    
        this.attachShadow({
          mode: "open"
        });
        
        this.shadowRoot.appendChild(template.content.cloneNode(true));
      }
      
      get name() {
        const slot = this.shadowRoot.querySelector("slot");
        const [name] = slot.assignedNodes();
        
        if (!name) {
          return ''
        }
        
        return name.textContent
      }
      
      connectedCallback() {
        console.log(this.name)
      }
    }
    
    window.customElements.define("place-holder", Placeholder);
    <place-holder>Hello</place-holder>

    Sidenote: The <slot> element will have a innerText whenever you add text inside the slot in the template as a placeholder.

    class ExampleElement extends HTMLElement {
      constructor() {
        super();
        
        this.attachShadow({ mode: "open" });
        this.shadowRoot.innerHTML = `
          <slot>Placeholder text</slot>
        `;
      }
      
      get placeholder() {
        const slot = this.shadowRoot.querySelector('slot');
        return slot.innerText;
      }
      
      connectedCallback() {
        console.log(this.placeholder)
      }
    }
    
    customElements.define('example-element', ExampleElement);
    <example-element></example-element>