cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
47.02k stars 3.18k forks source link

Add support for shadow dom #144

Closed chrisbreiding closed 4 years ago

chrisbreiding commented 8 years ago

Currently, Cypress fails for various reasons when dealing with the shadow dom. So far, I've seen three issues.

1) Cypress considers the shadow root to be a non-DOM subject.

cy.get("#root")
  .then ($root) ->
    return Cypress.$($root[0].shadowRoot)
  .find('.child-2')
  .should('have.text', 'I am child 2')

shadow root is non-dom

Moving the .find into the .then gets past this issue, but is not ideal:

cy.get("#root")
  .then ($root) ->
    return Cypress.$($root[0].shadowRoot).find('.child-2')
  .should('have.text', 'I am child 2')

It might be nice and maybe even necessary to have a convenience method like cy.getShadowRoot() that could be chained off other dom commands.

2) When you try to assert something on an element inside a shadow root, Cypress reports that the element is detached from the dom. This stems from the fact that $.contains(el1, el2) returns false when el1 is outside the shadow root and el2 is inside it. Some discussion here.

cy.get("#root")
  .then ($root) ->
    return Cypress.$($root[0].shadowRoot).find('.child-1')
  .should("have.text", "I am child 1")

shadow child detached from dom

3) Commands that cause Cypress to check if the subject is hidden by ancestors trigger infinite recursion and blow the call stack because it never hits the <body> or <html> elements while walking up the chain of ancestors.

cy.get("#root")
  .then ($root) ->
    return Cypress.$($root[0].shadowRoot).find('.child-2')
  .contains('button', 'Go')

shadow dom infinite recursion

StefanGussner commented 7 years ago

For anyone finding this issue on Google when searching for how to access the shadow Dom: I just this workaround until this feature is implemented:

it('.should - search by email', () => {
        cy.get('profile-search').then(search=>{
            const email = search[0].shadowRoot.querySelector('#email');
            email.value='test@example.com';
            email.dispatchEvent(new CustomEvent('change'));
        });
        cy.wait(200);
        cy.get('profile-search').then(search => {
            return search[0].shadowRoot.querySelector('profile-preview').getAttribute('uid');
        }).should('eq', 1);
    });
chachra commented 7 years ago

Any update on what might be best way to use Cypress on say a Polymer project nicely?

brian-mann commented 7 years ago

@chachra https://github.com/cypress-io/cypress/issues/830#issuecomment-339816720

leonrodenburg commented 6 years ago

Is there any update on this? The temporary solution proposed by @brian-mann in #830 doesn't seem to work in the newest versions of Cypress anymore. It throws a Maximum call stack size exceeded error when cy.wrap is called on an element that is in a shadow root. Might be related to issue 3 that @chrisbreiding mentioned above. We'd love to use Cypress for a large Polymer 2 app we're developing, but this seems to rule Cypress out as an option for now.

lawked commented 6 years ago

I would love to get some update on this too :)

QuentinDejean commented 6 years ago

Same here, we're heavily relying on shadow-DOM and use Cypress for our functional testing.

Thanks to @chrisbreiding we've figured a few ways to get around and select elements, but not having a proper support of shadow DOM makes it impossible to use .get(), and therefore to do anything related to forms.

LiberQuack commented 6 years ago

Updates? :sweat_smile:

nstogner commented 6 years ago

Would love to see this working! :+1:

Georgegriff commented 6 years ago

Commented on a similar issue here with my findings, although am able to find shadow Dom elements with JS, cypress thinks the element is not attached to the DOM anymore when I work with it https://github.com/cypress-io/cypress/issues/830

jennifer-shehane commented 6 years ago

This feature is still in the 'proposal' stage. No work has been done on this feature as of yet. We're a small team and as much as we'd love to work on everything, we have to choose what to work on based on a multitude of things.

soy-capitan commented 6 years ago

Just plus oneing this issue. I hope you prioritize this, because I'd like to be able to use cypress without having to petition every company I work for to rewrite their front end, since shadow dom is super common. I've been using it all of two weeks and I'm impassibly stuck because shadow dom can't be seen.

PabloHidalgo commented 6 years ago

Another +1 here!

Just plus oneing this issue. I hope you prioritize this, because I'd like to be able to use cypress without having to petition every company I work for to rewrite their front end, since shadow dom is super common. I've been using it all of two weeks and I'm impassibly stuck because shadow dom can't be seen.

I agree with you. Nowadays it's a common scenario big companies bet for WebComponents for build their front-end, and it's a shame we can't use cypress for testing :(

Hope it get's prioritized.

Snapu commented 5 years ago

+1

agasbzj commented 5 years ago

May I ask is there any update or workarounds? I'm testing ionic4 based apps and there are thousands of shadow doms in ionic4 components. :-(

vdamrongsri commented 5 years ago

I'm also on boat for cypress supporting shadow-dom for us stenciljs users. Any updates?

jennifer-shehane commented 5 years ago

No work has been done on this issue.

jackblackCH commented 5 years ago

What's the status quo here? Is it somehow possible to test WebComponents and/or ShadowDom?

LiberQuack commented 5 years ago

+1

FoxelFox commented 5 years ago

We are using Angular WebComponents and need this Feature.

JaySunSyn commented 5 years ago

Hi, I just started writing a plugin to support web components. It's still very raw but it works for me and my apps pretty well.

Check it out here https://github.com/JaySunSyn/cypress-daywalker

If I get enough GitHub stars, I'll continue šŸ™ˆ

Tested within a very big Polymer & lit app ... Performs well also with very deeply nested components. Creates an index of all registered custom elements. Not all CSS selectors are supported yet.

Adds some custom commands as well:

cy.get('my-el').call('close')
cy.get('my-el').call('search', ['jimmy'])
cy.get('my-el').dispatch('click')
cy.get('paper-input').setProp('Hello how are you?')
cy.get('paper-input').setProp('Question', 'label')
Georgegriff commented 5 years ago

@jaysunsyn it looks like your code might do similar things to this package I wrote for discovering deeply nested shadow roots with normal query selector syntax as if the shadow roots don't exist at all, might be worth checking out https://www.npmjs.com/package/query-selector-shadow-dom

JaySunSyn commented 5 years ago

Iā€™m not really querying the Dom to find an element but creating an index on registration time. But your lib looks interesting @Georgegriff

benjaminsweetnam247 commented 5 years ago

:+1: This issue is really starting to drive me up the wall

kamituel commented 5 years ago

I do realize Cypress' team is small and there's loads work to do, but still - I'm surprised this isn't a higher priority issue, considering how much of a blocker it is.

It pretty much forces us to pick either Cypress, or Shadow DOM, in any given project. Can't use both. (Unless you're willing to spend significant amount of time working on your own commands etc.)

And that's a Sophie's Choice - I want both, and both for good reasons [1] [2]. (IMO, Shadow DOM's benefits are more important)

Even worse - it doesn't seem like this situation is going to change any time soon :/

jennifer-shehane commented 5 years ago

@JaySunSyn Could you open a pull request to add this plugin to our documentation? Thanks! Instructions here.

JaySunSyn commented 5 years ago

Thanks @jennifer-shehane - I'll do that once I implemented some breaking changes and added better documentation for the plugin. Should be soon.

JPtenBerge commented 5 years ago

On developers.google.com where they discuss Web Components, they mention the idea of overriding attachShadow():

There's nothing stopping an attacker from hijacking Element.prototype.attachShadow.

A gist by praveenpuglia mentions the same thing:

Also, closed mode isn't a security mechanism. It just gives a fake sense of security. Nobody can stop someone from modifying how Element.prototype.attachShadow() works.

I believe this is a strategy that test frameworks can take advantage of as well. Simply create a div instead of a shadow root and testing can resume as always. Or create an open shadow root instead of closed. Or register that a shadow root is created there so you don't have to crawl through the entire DOM.

@jennifer-shehane: I understand that you guys are a small team and I don't want to be harsh here, but it's been three years. Your current mission statement clearly states:

The web has evolved. Finally, testing has too. Fast, easy and reliable testing for anything that runs in a browser.

Do Web Components not fit into that picture?

abramenal commented 5 years ago

Going to create a set of custom commands to bring support shadow DOM. More to come at https://github.com/abramenal/cypress-shadow-dom

JaySunSyn commented 5 years ago

Is this one not working for you guys? https://github.com/JaySunSyn/cypress-daywalker

JPtenBerge commented 5 years ago

@JaySunSyn: I'm gonna test it upcoming monday. @jennifer-shehane has indicated she's interested in your solution, so for the moment that's where my interest will be too. My comment from a couple of days ago was more out of amazement that it had been three years while I believe that the implementation code isn't that difficult. I've checked out your code just now, you're overriding customElements.define(), a very similar approach to overriding attachShadow() that I described.

JPtenBerge commented 5 years ago

@JaySunSyn: Ok, I've tested it! I couldn't get it to work with a simple page I'd setup myself, so I tried your example. Sadly, I get inconsistent results when running your example tests. At first, about half the tests failed. I commented one test out and after the second run, I noticed one of the tests that suddenly failed where it previously succeeded. Simply pressing F5 yielded different results again: Tests that passed the previous run suddenly failed and vice versa.

The failing tests all produce similar error messages:

TypeError: Cannot read property ... of undefined

I started commenting out more tests and noticed different results almost every run. And I've noticed, running only 1 or 2 tests, the success rate increases greatly. But as soon as I start to add more tests, even simple ones like nth and class, the failure rate rises to about 50%.

Has this example worked for you locally? Because one test seems to have an assertion error. nested custom element checks whether there are two paper-buttons on the page, but my-element really only uses one in its template. I've corrected this assertion locally during testing, but I can't imagine that this example testsuite has not resulted in errors at your end.

I'm running the example with polymer serve and .\node_modules\.bin\cypress open with the latest Polymer-cli and the latest Cypress installed.

abramenal commented 5 years ago

I'm really concerned about tweaking Cypress' existing commands, it feels same way as modifying host/native objects in JS. If I'm a newcomer to a certain object I would expect commands working in the way as official docs say, not really digging into all the packages finding what's actually affecting them.

JaySunSyn commented 5 years ago

@JPtenBerge I guess this is not the place to discuss the issues you have with the plugin but to answer your question, yes, the tests do work for me.

git clone git@github.com:JaySunSyn/cypress-daywalker.git
cd example
npm i
npm run test

If you need help or if you found bugs, pls create issues.

example-spec

kamituel commented 5 years ago

I wanted to share some thoughts on that. Initially, I presumed I needed to reach into the shadow DOM of custom element to assert on its contents and/or interact with it. I think some folks over here are trying to do the same thing.

However, since then I realized that might not be necessary, and in fact, might be counterproductive. Custom elements are all about encapsulation. The idiomatic way of using them in the web app is by setting properties and/or attributes, and listening for events. If the web app doesn't care about implementation details of a custom element, neither should that web app's tests. So instead of trying to mess with shadow DOM's contents, tests should also use custom element's properties and attributes.

Of course, custom element itself needs to be tested too. But this should be done separately, not in Cypress - because Cypress is an integration testing tool, not a tool for testing isolated elements.

For instance, consider a hypothetical custom element that implements a toggle button. It might be used like that:

let toggle = document.createElement('my-toggle')
toggle.onLabel = 'Switch On'
toggle.offLabel = 'Switch Off'
toggle.toggled = false
toggle.addEventListener('toggled', () => {
      toggle.toggled = !toggle.toggled
})

and its shadow DOM might look like:

<my-toggle>
    <!-- Shadow DOM begins -->
    <div class="some-fancy-wrapper">
       <button>Switch On</button>
       <button>Switch Off</button>
    </div>
   <!-- Shadow DOM ends -->
</my-toggle>

Testing a web app with this component in it might look like:

cy.visit(...)
cy.get('my-toggle').invoke('toggled', true)
cy.get('my-toggle').invoke('toggled', false)
...

No need to even attempt to .get() <button>'s inside of the shadow DOM.

Of course, the assumption here is that this toggle component is tested properly elsewhere, so we know that setting a .toggled property actually works. This can be done in a number of ways of course. I'm using Karma for now.

I think advantages of the above approach is kinda obvious, but just to reiterate:


Edit: forgot to mention, if your app relies on the custom element dispatching an event after a change, you might need to test it in a slightly different way:

cy.get('my-toggle').then($el => {
  $el[0].toggled = true
  $el[0].dispatchEvent(new Event('toggled'))
})
leonrodenburg commented 5 years ago

@kamituel That's a good suggestion, but what do you do if the button component you describe is itself part of another web component, let's say a form? You can't test inside the form (and according to your opinion we shouldn't). So you would need something else besides Cypress to test the form's behaviour. The form itself is part of a page, which also might be a web component and have shadow DOM. If we can only assert on the properties and events of the page component, testing the form's behaviour is again impossible. This chain goes up all the way to the root application component.

In the end, Cypress would be useless because we can only test the properties and event handlers of the main application component, which often doesn't have any inputs nor event handlers.

kamituel commented 5 years ago

@leonrodenburg yup, pretty much. So to be clear, an approach I described works in some cases, but certainly not in every case.

For us it seems to be a viable approach because (a) our web app won't be defining its own Custom Elements w/ Shadow DOM (b) a set of UI elements that will utilize Shadow DOM is limited to "widgets" (buttons, form elements, headings, etc - an implementation of a design system).

In our case web app will be tested with Cypress, and a library of Custom Elements won't (we're using Karma instead for now).

If your architecture is solely based on Custom Elements w/ Shadow DOM up to the level of the whole web app, then yeah, you'll need another solution.

JPtenBerge commented 5 years ago

@kamituel

Of course, custom element itself needs to be tested too. But this should be done separately, not in Cypress - because Cypress is an integration testing tool, not a tool for testing isolated elements.

I respectfully disagree with this.

Firstly, we're integrating web components with each other. That integration is interesting to test.

Secondly, Cypress describes itself as:

Fast, easy and reliable testing for anything that runs in a browser.

To me, that includes isolated elements. I'm not sure on this though, I've asked @jennifer-shehane last week, but haven't heard back yet. But I don't see why Cypress would limit itself this way.

Lastly, testing a custom element should indeed be tested in other ways, like with unit tests. But unit tests and end-to-end tests often have a bit of overlap. In general, it's not one or the other, people use both. Most functional logic will be covered with unit tests, but a simple check whether everything is rendered properly in browsers is very important as well.

If your architecture is solely based on Custom Elements w/ Shadow DOM up to the level of the whole web app, then yeah, you'll need another solution.

Which is the case for Polymer/LitElement projects.

ruimarques commented 5 years ago

@kamituel That's a good suggestion, but what do you do if the button component you describe is itself part of another web component, let's say a form? You can't test inside the form (and according to your opinion we shouldn't). So you would need something else besides Cypress to test the form's behaviour. The form itself is part of a page, which also might be a web component and have shadow DOM. If we can only assert on the properties and events of the page component, testing the form's behaviour is again impossible. This chain goes up all the way to the root application component.

In the end, Cypress would be useless because we can only test the properties and event handlers of the main application component, which often doesn't have any inputs nor event handlers.

This is exactly what would happen in a micro frontends architecture where whole pages can be a web component. My team is in a the process of choosing a testing tool and we are about to commit to choose TestCafe instead of Cypress mostly because of better shadow dom support.

grenma-ld commented 5 years ago

I'm sure you've heard this hundreds of times: many of us are looking for a Selenium alternative and as a result are predisposed to use Cypress, but this issue (and to a lesser extent multi-browser and tab support) is a deal-breaker. Any transparency on this would be helpful.

flotwig commented 5 years ago

@grenma-ld It's something we want to add, but nobody from the Cypress team is currently working on this. There's no PR open for it from the community either.

For working with the shadow DOM today, you can try this user-created plugin that adds support: https://github.com/JaySunSyn/cypress-daywalker

grenma-ld commented 5 years ago

Haven't been able to get daywalker to work yet - will keep trying and perhaps reach out, but unless it works well we'll have to fall back to Protractor/Selenium

GlenHughes commented 5 years ago

Such a shame this great tool won't be made to work with the ShadowDom, lots and lots of apps are starting to use this great feature and cypress supporting it would be a great feature :)

abramenal commented 5 years ago

@grenma-ld @GlenHughes and anyone who are interested I think until there is no official support for shadow DOM we can make a set of custom commands to make our stuff work.

I've created an initial set of shadow'ish commands for querying, asserting elements and working with events. It'll be great if we can review that together, create bugs & decide what are must-have commands that majority of the users need. I'm fully okay to support the development of anything related to that.

https://github.com/abramenal/cypress-shadow-dom

Note: I saw daywalker package, but don't like both the implementation approach used and the exposed commands structure (especially since it's a try to override cypress' ones).

Looking forward your feedback! Just remind that's not really a ready package but a place to start with something šŸ˜ˆ

GlenHughes commented 5 years ago

Hi @abramenal,

I've actually started to use TestCafe for the moment but what you've come up with looks brilliant. I will give that a go and see how it works. I'm happy to chip in when I can :)

covaHL commented 5 years ago

any of the above solutions works for lightning components rendered by salesforce, if someone has faced this issue before and have a workaround the guidance should be more than welcome!!

TomCaserta commented 5 years ago

So from a brief look into it, it seems like jQuery may need to be modified as the methods there are not 'ShadowDOM aware', there are various places where cypress uses such methods to check for visibility and if the element is on the page (eg. it uses parent and contains method for these).

Patching these should be quite simple just making sure it's traversing up through shadow boundaries.

I think a real solution here would also need to not use any special notation for traversing through shadow boundaries, the get method imo should be extended to perform something similar to @Georgegriff's https://www.npmjs.com/package/query-selector-shadow-dom, perhaps a deep: true option could be added to the options parameter.

Will take a look further at some point in the future to figure out exactly what needs to be fixed to make this work as expected. Would you be accepting PR's for this feature?

Note: As a work around, providing you are using the ShadyDOM polyfill, you can actually use window.ShadyDOM = { force: true } to force the polyfill to load. This means that your site is no longer using real shadow dom and you can do your tests as normal.

Of course though this means you're testing against something your users won't actually see and you risk having to rewrite a lot of tests if the Cypress devs decide on a different syntax for piercing shadow dom in the 'get' method, but it's something.

TomCaserta commented 5 years ago

So after revisiting this it seems like there may be quite a bit of work here, I was working with just trying to get Cypress to accept elements in the shadow dom without crashing with Maximum callstack errors etc.

  1. First up jQuery needs to be updated as there have been some fixes in regards to working with Shadow DOM.

  2. Everywhere that jQuery.parent is used it needs to be changed as by default as soon as jQuery encounters a Document Fragment (nodeType 11) it will just return null as the parent as it expects it's not connected to the DOM. What we should do in this scenario is traverse the DOM ourselves and check if node type is fragment node and then check if there is a host property on this to continue traversing up.

  3. isChild (maybe - not certain yet) and other similar methods need to actually do the reverse and check if any of the children have a shadow root, this is slightly more tricky and would become quite resource intensive I imagine.

  4. Mouse events & Keyboard events & Other UI events should set the composed flag to true as these by default can pass through the ShadowDOM.

  5. getDefaultButtons in type.js needs to find them inside the shadow dom.

  6. getElementAtPointFromViewport needs to jump into shadow root and traverse down until it finds the first element without a shadowRoot. This gets tricky as you need to account for slotted text nodes that will be the element 'on top' but actually is inside the child component meaning you also need to check if the element thats supposed to be covering the child is actually a parent node.

  7. type() needs to support custom input web components. Thought this would be as simple as just adding configurable additional selectors to isTextLike however it gets complicated with the getNativeProp method being called.

  8. Similar to 7, with web components that extend button elements, cypress needs to check the 'disabled' attribute/property to determine if it's clickable yet.

  9. traversals.coffee needs to be completely re-written to support shadow dom or majority of the jquery methods there need to be modified.

I started work on these here: https://github.com/cypress-io/cypress/compare/develop...TomCaserta:issue-144?expand=1

Not much there at the moment but it now doesn't throw errors when picking up elements in the shadow dom but I imagine there are tons of other issues around. It would be great to get some input from some Cypress devs on how this issue could be tackled, right now it seems like actually implementing this could introduce some breaking changes which is not ideal.

mohammedalnuaimi commented 5 years ago

@abramenal I've installed the cypress-shadow-dom but getting an issue with locating the elements, I keep getting 'shadowRoot' of undefined

that's how I'm calling it

cy.visit('/url')
cy.shadowGet('css locator - root element')
  .shadowFind('css locator')
  .shadowTrigger('click')
abramenal commented 5 years ago

@mohammedalnuaimi have you seen the example app covered with test cases? https://github.com/abramenal/cypress-shadow-dom/tree/master/example

If you still seeing the issue, please feel free to submit the issue to the plugin's repo so we can take a look together on your specific setup, tests and issue you're getting. Current place is not the best one to discuss my plugin šŸ˜ˆ

mohammedalnuaimi commented 5 years ago

thanks @abramenal for sending the example over, will have a look and raise an issue if required