Cypress is just ...

Cypress allows any developer to fundamentally change how it looks and works, because it is just JavaScript code running inside a browser backed by a Node app.

One thing I have to repeat again and again to everyone willing to liste, is that Cypress architecture is fundamentally different from Selenium or WebDriver. Cypress runs right inside the browser next to your web app. And Cypress is just JavaScript, like your web app (after maybe transpiling the source code). And if you know how to build a web application, you can change how Cypress looks and behaves because Cypress user interface is a web application itself. Because Cypress is also a Node application, from the tests you can jump to the operating system and do everything you might want. Let's see how it all comes together.

Cypress is just JavaScript

Cypress tests are written usually in JavaScript, CoffeeScript or TypeScript. Ultimately everything gets transpiled to JavaScript, and runs in the spec iframe in the browser. Modern browsers understand modern JavaScript (and missing features can be polyfilled for your tests), so your tests can take advantage of it.

Take ES6 proxies for example. We can use a proxy to intercept calls to the global cy object and create convenient methods for finding elements by test id attribute. The following code snippet comes from cypress-get-it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
global.cy = new Proxy(global.cy, {
get (target, prop) {
console.log('getting prop', prop)
if (/^get\w+/.test(prop)) {
const words = getSomethingToWords(prop)
const attribute = getAttribute(words)

return selector => {
cy.log(`${prop} "${selector}"`)
return target.get(`[${attribute}="${selector}"]`, { log: false })
}
} else {
return target[prop]
}
}
}

So when we call cy.visit('...') from now on, it goes to the "real" cy.visit. But if we call any method that starts with cy.get... then we convert the method name like cy.getFooBarBaz("value") to the data attribute selector and call the existing method cy.get('[foo-bar-baz="value"]).

For page that looks like this:

1
2
3
<div data-test-id="foo">foo</div>
<div data-test="bar">bar</div>
<div test-id="baz">baz</div>

we can write method names that express actual data attributes and are easy to read

1
2
3
cy.getDataTestId('foo').should('have.text', 'foo')
cy.getDataTest('bar').should('have.text', 'bar')
cy.getTestId('baz').should('have.text', 'baz')

Successful elements

Should you use cypress-get-it? Probably not. You better use small utility functions without any magic.

1
2
3
4
5
6
const ti = s => `[data-test-id="${s}]`
const t = s => `[data-test="${s}]`
const i = s => `[test-id="${s}]`
cy.get(ti('foo')).should('have.text', 'foo')
cy.get(t('bar')).should('have.text', 'bar')
cy.get(i('baz')).should('have.text', 'baz')

You can even overwrite cy.get using custom Cypress command and invent your own syntax (in addition to the built-in jQuery selectors)

1
2
3
4
5
6
7
8
9
10
// all selectors that start with "=" are going to become "data-test-id" selectors
Cypress.Commands.overwrite('get', (get, selector) => {
if (selector.startsWith('=')) {
const value = selector.substr(1)
const s = `[data-test-id="${value}"]`
return get(s)
} else {
return get(selector)
}
})

You have a choice, because it is just JavaScript.

Cypress runs in the browser

Cypress is controlling a real browser when it runs your tests. In the browser window, there are 2 iframes: app iframe and spec iframe. The app iframe is holding the web application. The spec iframe loads the bundled test code.

Cypress architecture

The spec iframe has no width or height, since it has no visual elements. Instead it sends all events that happen during a test to the top window where Cypress web application is drawing the Command Log. You can open DevTools and inspec the iframes yourself.

Iframes in the elements panel

The most immediate result of this architecture besides being able to control application directly via app actions, is that the test code can modify the Cypress user interface. Literally, your spec code can even use JSX right away because Cypress UI is a React application and our browserify bundler transpiles JSX.

1
2
3
4
5
6
7
8
const ReactDOM = require('react-dom')
it('injects dynamic React component and it works', () => {
cy.visit('index.html')
cy.get('#app').then(el$ => {
const welcomeGleb = <Welcome name="Gleb"/>
ReactDOM.render(welcomeGleb, el$[0])
})
})

Once you realize that the spec JavaScript code can control the web application it is running in, the world is your oyster. For example, you will no longer need to wait for the Cypress dev team to add color theme support. You can just do it in user space.

Cypress Halloween theme

Find the source code and two dark color themes in cypress-dark.

Cypress has Node backend

Cypress tests are running in the browser, but can call the backend code that runs on Node using cy.task command. Anything you might want to do on the host system can be done from Node. Read and write files, work with a database, send smoke signals - anything Node can do, your tests can do too.

Let's put everything we just saw together. Running a single test, or skipping a test from the Cypress UI has been a common feature request. But do we need to change the core of the test runner to be able to do it? Can we do it ourselves (in a hacky way)? We want:

  • when all tests have finished, put a button "Skip" next to each test name in the Command Log
  • when a user clicks on "Skip" button, we can read the spec file and change it by adding it.skip for that test
  • save the changed file on disk, and Cypress will pick up changes, rerunning the tests

So let's do this. You can find the solution in cypress-skip-and-only-ui repo. Drawing buttons after all tests have finished is somewhat tricky because we have to compute the full test title from UI elements by walking through the DOM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
after(() => {
// finds the root node of all tests from Mocha runnables
const runnable = cy.state('runnable')
const root = getRootSuite(runnable)
const titles = getTests(root)

$.find('.runnable-title').map(rt => {
// walk through the DOM to find full title
const uiTitle = findParentTitles(rt) || []
uiTitle.reverse()

// match test title from DOM with test titles found from the runnables
if (titles.some(testTitle => Cypress._.isEqual(testTitle, uiTitle))) {
// add UI buttons for this test
addOnlySkipButtons(rt, uiTitle, Cypress.spec)
}
})
})

Find the rest of the code in src/support.tsx. The final result looks like this:

skip, only and reset buttons

When you click on a button, like "skip" for example, it sends a message using cy.task to the Node backend. The message includes the spec filename (provided by Cypress) and the full test title.

1
2
3
4
5
6
7
8
const onClickSkip = () => {
cy.task('skipTests', {
// like '/foo/bar/cypress/integration/spec.js'
filename: spec.absolute,
// like ['several tests together', 'inner', 'has deep test']
title: title
})
}

The Node handles the skipTests command by loading the spec, creating an abstract syntax tree, walking it to find CallExpression with Identifier = "it" and then rewriting that particular node. Hint: use module called falafel for this, it is great. Find the code to do this in src/task-utils.ts. Note: I am transpiling TSX to plain React.createElement using TypeScript before publishing cypress-skip-and-only-ui to NPM, because Cypress bundler does NOT transpile node_modules. The final result: Cypress UI with my buttons that modify the specs on the fly, and Cypress rerunning the tests on chance.

skip, only and reset buttons in action

Beautiful.

Conclusion

If you know how to make a web application using JavaScript, HTML and CSS, you can:

  1. Write good end-to-end tests using Cypress.io test runner.
  2. Customize how Cypress looks and behaves because Cypress is just a JavaScript code running inside a web app and on Node backend.

And if you are just beginning your web development journey, take a look at Cypress testing tutorials. They will help you get better at both testing and at web development in general.