jamesshore / quixote

CSS unit and integration testing

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for cypress.io

greyepoxy opened this issue · comments

Thanks James for your great work on this library! Really appreciate that you are pushing the bounds of what people think is possible

Recently I have been using cypress.io to do my UI testing and have been greatly enjoying how it doesn't require arbitrary waits, allows you to visualize everything that occurred in your test during or after it runs, and also encourages writing fast tests.

I was curious on your thoughts for adding quixote support to cypress?
I can think of a couple different ways of integrating,

Expose a way of creating QElement's from a dom node

Since cypress already hosts the UI under tests in an iFrame, there is no need for creating a corresponding QFrame. If a QElement from dom node constructor was exposed then after getting nodes in cypress could create the QElements and then assert as one would today (using descriptors) and cyrpess's should command https://docs.cypress.io/guides/references/assertions.html#Should-callback.

I believe this would be relatively straight forward, looking at the source https://github.com/jamesshore/quixote/blob/release/src/q_element.js#L13, I believe we just need to remove the frame from the constructor and everytime you want to access the document or body (like here https://github.com/jamesshore/quixote/blob/release/src/q_element.js#L47) use ownerDocument and then defaultView or parentView instead.

As a Custom Assertion library

I saw the discussion in the other thread #47 about updating the assertion library and one of the ideas called out was to create custom chai assertions. Assuming the object being asserted upon is create able from just dom nodes I believe this would be easy to use in cypress https://docs.cypress.io/guides/references/assertions.html#Adding-New-Assertions.

Can get a QFrame from a cypress test frame

Not sure you can get at the iFrame directly but cypress does expose commands to get the window or document https://docs.cypress.io/api/commands/window.html#Syntax.

As a Plugin

There are a bunch of classic visual testing tools integrated as plugins https://docs.cypress.io/plugins/#visual-testing. Not exactly sure if that makes sense with quixote, do not really know how the integration would work.

Thanks for your detailed suggestion. I'm not personally familiar with Cypress, but I'm open to changing Quixote to make it easier to support it.

It looks like the main thing we would need is the ability to make QElements. Currently, to make a QElement, we need two things: the DOM node (for obvious reasons) and the QFrame it lives in. You suggested that we remove the QFrame. Currently, we use the QFrame in these places:

  1. QElement, where it's used a) in getRawStyle() work around a Firefox bug; and b) in parent() to detect the body element so it won't be returned.

  2. ElementEdge, where it's used to detect if an element is rendered.

  3. ElementRenderedEdge, where it's used to find the page borders, so we can tell if an edge of an element is off-screen. (ElementRenderedEdge is in turn used by ElementRendered to see if the whole element is off-screen.)

I'm not convinced we can get rid of the QFrame requirement. If you see something I don't, let me know. I haven't studied it in depth.

Other than the QFrame issue, this seems fairly straightforward. We'd just need a QElement.fromDomElement() factory function to go along with our existing QElement.toDomElement() method. We could also make a corresponding QFrame.fromDomElement() factory.

Thanks James, appreciate you taking a look. I opened a pull request demonstrating the removal of QFrame. Let me know what you think

What do you think about having a method such as quixote.frameFromDom(domElement) instead of having a separate QContentHost class? It would allow you to make a QFrame out of your Cypress iframe.

hmmm I had not thought of that. I believe it would work but not ideal since cypress does not expose the frame as part of its api (just window and document ). So would have to figure out a way to get at it (maybe a little fragile?) or ask them to extend their api.

Would you like me to look into that?

No need, I'm working on it now. I think frameFromDom() would only work if we could somehow get the frame from an arbitrary element inside the frame, which may not be possible. Or if we don't need the actual iframe element, which might be. The main thing I want to avoid is having a separate QContentHost class--it makes the API more complicated and doesn't benefit the majority of users.

Yeah I completely agree, curious to see what you figure out

Looking at it further, the only thing in QFrame that needs the actual iframe element is frame.resize(). As your code shows, everything else of interest can be done with nothing more than domElement.ownerDocument. (Or have I missed something? Let me know.)

So what I'm thinking is that I'll just modify QFrame so they can be constructed from an <iframe> element or domElement.ownerDocument. In the latter case, the resize() method will fail. Then quixote.elementFromDom will create the QFrame using domElement.ownerDocument. All the existing QElement code can thus remain the same.

Here's my plan:

  • Refactor QFrame to make it easier to work with.
    • Factor quixote.browser into its own module.
    • Add a QFrame factory method that takes an iframe element.
    • Factor QFrame's frame creation logic into quixote.js.
  • Add quixote.frameFromDom(). It will be smart enough to work with either an arbitrary element or an actual frame element.
    • Add a QFrame factory method that takes a document element.
    • Modify QFrame methods to fail fast when the iframe element is not available.
      • resize()
      • remove()
      • toDomElement()
    • Modify QFrame methods to work with provided iframe.
      • reset()
      • reload()
    • Implement quixote.frameFromDom().
  • Add quixote.elementFromDom(). It will take an optional QFrame parameter. If it doesn't get one, it will call quixote.frameFromDom().

Let me know what you think.

hmmm there are a couple of methods that I do not believe make sense without the iFrame (or would behave differently depending on if constructed with the frame or the ownerDocument),

  • frame.remove
  • frame.reload
  • frame.reset
  • frame.toDomElement
  • frame.resize

Cannot say I am crazy about matrixing the QFrame logic depending on how it was constructed. I think I would prefer the more explicit objects/interfaces. For a consumer, knowing which functions are valid depending on how QFrame was constructed will be something new to figure out. Seems like that might lead to its own confusions?

How about the following adjustment to #56,

  • In the documentation make it more explicit that QFrame is a QContentHost (then we can also get rid of the duplicate method documentation between QFrame/QContentHost, just point people in QFrame's documentation to the QContentHost documentation).
  • Make QFrame.toContentHost "internal" so that we do not change the QFrame api at all

Then that will mean there is just the two new public api methods quixote.elementFromDom and QElement.host(). Or I if we want to just do a single new public api then something like quixote.hostFromDom would also work (although QElement.host() would still be needed internally).

Not sure if this would address your core concerns or not, what do you think?

I prefer explicit interfaces too.

My concern here is that this is a very narrow use case. Most people will just use quixote.createFrame directly. I want to make common things easy, using terminology people are familiar with. I want to support niche use cases too, but not at the expense of introducing jargon ("content host") that affects the mainstream case.

I'm going to take a second look at QElement and see if there's a way of removing the dependency on QFrame entirely. That would sidestep the entire issue.

Okay, having looked at it further, I'm leaning toward using your QContentHost but not exposing it to the public. Instead, QFrame will encapsulate it and QElement will use it in place of the current frame. I think this is a fairly minor change to your code in #56. I'll give it a try and see where it takes me.

I think I'll still provide a quixote.frameFromDom as planned, for completeness, but it will just take an <iframe> element rather than magically working with a regular element.

ahh okay so if I understand correctly, two new public apis

  • var element = quixote.elementFromDom(domElement)
  • var frame = quixote.frameFromDom(iFrameDomNode)

No public apis that expose the QContentHost (like QElement.host does today)

Assuming I understand correctly, 👍 that should be just a small modification and should totally work, thanks for exploring these different options!

Yes, exactly.

This has been integrated and will be in the next release.

I've decided not to add quixote.frameFromDom() this time. QFrame is kind of a mess and there's weird interactions with reset() and reload() that I'm not sure how I want to handle. Since nobody's asking for it and I don't have a lot of spare time right now, I'm going to drop it.

Released in v0.15.

Awesome thanks James! 🎉

Thanks for your hard work!