Search code examples
cypressweb-component

How To Query Through <slot> Using Cypress While Testing Web Components


After years of testing one global DOM for end-to-end testing, I'm finding it very difficult, if not impossible, to test web components that use slots. Before I explain the problem, I want to say that I cannot change the generated markup to improve things as they are.

<wc-1 attributes-etc="">
  <wc-2 attributes-etc="">
    <slot>
      <wc-3 attributes-etc="">
        <slot>
       ...eventually get to an input...
         <input type="text" name="firstName" />

There are a buttload of nested web components from some kind of form builder, and there are also plenty of slots used. The web components have attributes but the slots never do, so I use the web component name for querying.

document.querSelector('wc-1')
  .shadowRoot.querySelector('wc-2')
  .shadowRoot.querySelector('slot')

// Yields <slot>...</slot>

All fine to this point and Cypress has a .shadow() command I used, but I'm testing with just devtools here to see all the properties the slot has.

document.querSelector('wc-1')
  .shadowRoot.querySelector('wc-2')
  .shadowRoot.querySelector('slot')
  .shadowRoot

// Yields "null".
// I don't know how to get to the .lightDOM? of wc-2?

Any property I try ends up being null or having 0 elements in the returned value. Using other front-end tools and the global DOM, I can always cy.get('div[data-testid="the-nested-element-i-want"]').type('important words') in one command.

So my main question is: How do people test these things once web components start piling up? Or don't do this and just test the web components in isolation/unit tests since it's so hard to query the nested shadow DOMs?

The main goal is to eventually get to a form input to cy.get('input[name"firstName"]').type('John'). Can someone give me the chained docuement.querySelector() command to get to <wc-3> in my example?


Solution

  • The answer involves assignedNodes(): https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes

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

    It made no difference for me to use that vs. assignedElements(). So, all you have to do is use that method once you've queried down to the slot you need. For my example, the answer is:

    const wc-3 = document.querySelector('wc-1').shadowRoot
      .querySelector('wc-2').shadowRoot
      .querySelector('slot').assignedNodes()
      .map((el) => el.shadowRoot)[0]
    

    And then you can keep going down the chain...I know I only have one un-named slot, so that's why I grab it from the returned .map().

    Props to this Q&A for pointing me on the right direction: Web components: How to work with children?