Closed NicholasBoll closed 6 years ago
Related documentation issue: https://github.com/cypress-io/cypress-documentation/issues/110
Thanks @jennifer-shehane
Playing around with snapshot
is very useful. Combined with { log: false }
of cy
commands gives me a smarter snapshot experience.
For example, I have a command that renders React components (also did it with Angular components) where it will add an isolated component at a mount point for per-component testing which is much faster than refreshing the page.
Cypress.Commands.add('mount', name => {
const log = Cypress.log({
message: 'Mounting ' + name,
name: 'mount',
})
cy
.window({ log: false })
.then((win: Window & { [key: string]: any }) => {
win.unmountComponentAtNode(win.document.getElementById('test'))
win.render(
win.react.createElement(win.RenderWithStore, {}, win.react.createElement(win[name])),
win.document.getElementById('test'),
)
})
.then(() => {
;(log as any).snapshot().end()
})
})
The snapshot allows me to log just the mount
command with a snapshot before cy.mount
and after rather (before blank page, after mounted component). The log as any
is to get around the Cypress.log
returning void
according to the type definitions.
Yes - this is good. This is what we imagined using Cypress to be able to do pure component based testing. It's a little hacky visiting a blank page - we felt like there needs to be something directly in Cypress to "zero out" the application area.
There's a bunch of improvements I wanted to make to Cypress.log()
before officially documenting it. It starts to get complex in the sense that it requires other supporting API's to be fully baked before it all "fits together". If you look at our own commands you'll see the full usage of it.
PS. you can name your snapshots by passing them a name - and also provide the "next" name as well, as whenever a command is resolved we automatically take a snapshot of it at the correct time.
Can you also try just requiring your components directly in your spec files? Like in a unit test.
I imagine that you are making all of the components accessible via the page served by cy.visit
but wouldn't it be easier just to require them and mount them programmatically?
This test is written against the documentation of a component library. The docs are just a SPA with an index.html
and can be served from any file-base webserver (python -m SimpleHTTPServer
, nodes http-server
, Jenkins artifacts, etc). There is a special test
route that just loads with a blank page and a <div id="test"></div>
which the mount
command uses. The hack is the documentation exposes globals for the mount
command to use. The before
of a file visits this route and the beforeEach
mounts it. I brought this concept from an Angular component library that used Protractor which did the same thing through Selenium's evalScript
. It increased test speed by about 10x (load page once per file rather than once per it
block)
I suppose we could actually require the components directly. We'd be bringing in all of React, ReactDOM and component source code with the test file as well as with the page. I'd have to experiment with what that does to the loading of a spec (probably increase slightly). Also not sure how this will work with React. I think it will basically be 2 React libraries in the page?
No - what I'm suggesting is this. You visit a blank html page with nothing in it. Zero, nadda. Just your basic <html><body><div id='root'></div></body></html>
.
You then require the react component in your spec file, and then mount it / render it onto #root
. There will only be one react application - the component will live in your spec file, but render to the "application area".
This gives you the benefit of "poking" at the component with cypress commands, or programatically interacting with the component directly since its a direct in memory reference in your spec file. You're basically doing the same thing albeit in a much more indirect way (by exposing them on window
).
const myComponent = require('./lib/my-component')
describe('my component', () => {
before(() => {
cy.visit('/blank.html')
})
beforeEach(() => {
cy.get('#root')).then(($root) => {
// mount the component directly here
// on root
})
})
it('foo', () => {
// use cypress commands or
// the myComponent reference directly
})
})
Oh, I see. Still have the exposed window variables for the mount, just pass a reference to the component rather than the name of it (which is pulled off window)
I don't believe you need to expose anything in the remote window. All of it can be done directly from your spec file.
The only thing specific to your application in the remote window would be CSS styles. That's it. The rest of it should all go in the spec file itself.
I would like to wrap all of these up into a react specific plugin.
cy.mount(componentReference)
// mount a component in the remote windowcy.component('nameOfComponent')
// find component in the DOM instead of using cy.get('selector')
Interesting. There are some challenges there. I went down a rabbit hole of configuration changes to make TypeScript and Webpack happy. Our documentation examples don't use relative paths, but rather the library's package name so example code can be copy/pasted into an app and "just work".
As I suspected, the spec files just run in the browser and require a bundler for import/require statements to work. So my component requires React which also gets bundled per spec file (unless I expose React
on window and have the bundler reference window.React
rather than bundling per test).
For now it is more work to get a reference through JS than it is to just expose the component on window.
I'm interested in how you'd solve this with a mount plugin.
I have to say the ability to customize the log and snapshot experience is awesome! Our previous CodeceptJS tests have very messy output (every command was a log entry) and made it almost impossible to figure out what was going on in a test.
I got past the resolution issues, but our components uses CSS modules. Basically the entire app is being built per spec file. The only way to share that code is through Webpack's externals
which requires those modules to be exposed on window
anyway. Angular 1 was a bit easier since I just exposed angular
on window and that exposed the whole app. I could then use angular.module
to get at whatever I needed. React is a bit different - component references aren't exposed to external processes.
I'm sure it is possible since the React dev tools do it, but I'm not sure how.
I'm interested in how this might work with a React plugin. For now I'll keep our app just using the global components exposed through the documentation.
You can access react internals through properties directly on the DOM elements.
Here's how testcafe does it https://github.com/DevExpress/testcafe-react-selectors/tree/master/src/react-16
From that example I can see how given a DOM element, I can get the component's constructor (the component class): $0.__reactInternalInstance$bea5aytb3j4._currentElement._owner._instance.constructor
. I think that would require the component to be rendered already. I could then grab the component reference, unmount and now mount whenever I need. That might not be a bad compromise. Our examples do have individual routes to test them in isolation of the rest of the documentation. At that point I could just manipulate the route to force the App to remount.
If a component isn't rendered, I'm not sure how to access its constructor.
Right, it would already have to be rendered. But that is what we will do for the custom cy.component
command which instead of querying the DOM, it will search and find the component by name.
The cy.mount
stuff won't help us here but I imagine there is some middle ground for how to do this. The direct references would be ideal as this would come very close to being a true unit test while including the power of Cypress.
Released in 3.0.0
.
The implementation of
Cypress.log
returns a chainable interface with methods likeinvoke
andsnapshot
, etc. The type definitions just returnvoid
. The documentation doesn't say much.I'm wondering what should the public interface returned by
Cypress.log
be? Should they reflect the implementation and the docs also get updated?Is this a Feature or Bug?
Not sure