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

2

5

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?

Tater answered 25/9, 2021 at 15:20 Comment(0)
D
6

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>
Devondevona answered 26/9, 2021 at 19:50 Comment(0)
C
2

Building off of the accepted answer (and thank you for that helpful info):

Just in case someone else wanted to test more complex slotted elements, here is my test for slotting an SVG icon component into a button named slot:

    it(`should render icons when passed to the button`, async () => {
      element = await fixture(html`<my-button text=${'click me'}>
        <my-icon name="article" slot="iconLeft"></my-icon>
      </my-button>`);
      const slot = element.shadowRoot!.querySelector('slot');
      const slotContent = slot!.assignedNodes()[0] as HTMLSlotElement;
      expect(slotContent.shadowRoot!.querySelector('svg')).toBeInTheDocument();
    });
Cravens answered 15/9, 2022 at 21:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.