testing-library / jest-dom

:owl: Custom jest matchers to test the state of the DOM
https://testing-library.com/docs/ecosystem-jest-dom
MIT License
4.45k stars 401 forks source link

Shadow DOM support? #340

Open chadwtkns opened 3 years ago

chadwtkns commented 3 years ago

Hi Im curious if Jest DOM can support Web Components using the Shadow DOM

For example Ive got the following test

const defaultElement = document.createElement('custom-element')
expect(defaultElement.shadow.querySelector('template')).toContainHTML('<slot>')
// also tried
expect(defaultElement.shadow.querySelector('template')).toContainElement(defaultElement.shadow.querySelector('slot'))

and the test is complaining that received is not an HTMLElement or SVGElement. My understanding is that the shadow DOM doesn't appear to be supported by jest-dom so I am curious if this is something that could be considered. I know its not a trivial thing to add and depending on how much time I get I'm open to giving a shot to implement it myself too but I'd like the maintainer's thoughts on this. Thank you!

gnapse commented 3 years ago

I have almost zero experience with shadow dom and web components, so I can comment very little on what the cause is or how to fix it.

That being said, I do not object to making it work as long as the changes needed for it are manageable in terms of future maintenance and of course that they do not affect the existing functionality. I'd be open to see a draft of what it would take, say, with a single custom matcher first, before we set out to make all custom matchers compatible with these requirements.

just-boris commented 3 years ago

Hello!

@chadwtkns I tried to create a simple repro and it worked for me

class CustomElement extends window.HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.shadowRoot.innerHTML = '<template><slot /></template>'
  }
}

window.customElements.define('custom-element', CustomElement)

test('shadow dom support', () => {
  const defaultElement = document.createElement('custom-element')
  expect(defaultElement.shadowRoot.querySelector('template')).toContainHTML(
    '<slot>',
  )
})

There could be additional gotchas you will need to watch out:

  1. Shadow DOM works only in Jest 26, previous versions use older JSDOM
  2. The cannonical name of shadow root property is defaultElement.shadowRoot, not defaultElement.shadow
  3. defaultElement.shadow.querySelector('slot') returns null, because the content of <template> element is passive and not queryable
  4. There could be something with your custom-element implementation, it is hard to tell without the full demo.

It would be helpful if you create a demo using official codesandbox template: https://codesandbox.io/s/5z6x4r7n0p

ThijSlim commented 2 years ago

Just recently a package appeared which makes it really easy to interact with the shadow Dom using Testing Library methods: https://www.npmjs.com/package/shadow-dom-testing-library

KonnorRogers commented 1 year ago

Hi. I'm the maintainer of said package, I needed it for work and figured I'd share with the world 🤷‍♂️

milky2028 commented 9 months ago

For anyone in the wide world future seeing this, there is an easy way to port queries to check shadow DOM that I haven't seen anywhere else on there internet.

You can do a slightly different version of this for all different types of queries or create some sort of factory to do so.

import { AllByRole, buildQueries, ByRoleMatcher, ByRoleOptions, queryAllByRole } from '@testing-library/dom'

const _queryAllByRoleDeep: AllByRole = function (container, ...rest) {
  const result = queryAllByRole(container, ...rest) // replace here with different queryAll* variants.
  for (const element of container.querySelectorAll('*')) {
    if (element.shadowRoot) {
      result.push(..._queryAllByRoleDeep(element.shadowRoot as ParentNode as HTMLElement, ...rest))
    }
  }

  return result
}

const [_queryByRoleDeep, _getAllByRoleDeep, _getByRoleDeep, _findAllByRoleDeep, _findByRoleDeep] = buildQueries<
  [ByRoleMatcher, ByRoleOptions?]
>(
  _queryAllByRoleDeep,
  (_, role) => `Found multiple elements with the role ${role}`,
  (_, role) => `Unable to find an element with the role ${role}`
)

export const queryAllByRoleDeep = _queryAllByRoleDeep.bind(null, document.body)
export const queryByRoleDeep = _queryByRoleDeep.bind(null, document.body)
export const getAllByRoleDeep = _getAllByRoleDeep.bind(null, document.body)
export const getByRoleDeep = _getByRoleDeep.bind(null, document.body)
export const findAllByRoleDeep = _findAllByRoleDeep.bind(null, document.body)
export const findByRoleDeep = _findByRoleDeep.bind(null, document.body)
KonnorRogers commented 9 months ago

@milky2028 thats roughly what shadow-dom-testing-library is doing for queries.

https://github.com/KonnorRogers/shadow-dom-testing-library/blob/main/src/shadow-queries.ts

https://github.com/KonnorRogers/shadow-dom-testing-library/blob/main/src/deep-query-selectors.ts

as I worked on it there were additional warts:

https://github.com/KonnorRogers/shadow-dom-testing-library/blob/main/src/trick-dom-testing-library.ts

https://github.com/KonnorRogers/shadow-dom-testing-library/blob/main/src/pretty-shadow-dom.ts

There's probably more I've forgotten.