How to Test an Application that Changes a CSS Variable

March 17, 2020

By Gleb Bahmutov

The App

Let's take an application that has an <input type="color"> element. When the user picks a new color, the application changes a CSS variable which controls the background color. In action, it looks like this:

The application in action

The HTML markup below has only the input color element.

<head>
  <link rel="stylesheet" type="text/css" href="app.css" />
</head>
<body>
  This is the body of the document
  <div>Change color using this color input
    <input type="color">
  </div>
  <script src="app.js"></script>
</body>

The app.css file uses CSS variables to control the background color

:root {
  --main-color: #fff;
  --background-color: #000;
}
body {
  color: var(--main-color);
  background-color: var(--background-color);
  font-size: xx-large;
}

Finally, the application code app.js reacts to the change event and sets the new CSS variable value

document.querySelector('input[type=color]')
  .addEventListener('change', (e) => {
    console.log('setting new background color to: %s', e.target.value)
    document.documentElement.style.setProperty(
      '--background-color', e.target.value)
})

Tip: find the application and test code in repository cypress-example-recipes under "Testing the DOM" recipes.

Tests

In our first test, we will ignore how the application implements the background color. Instead we will confirm that the change works.

it('starts with black background', () => {
  cy.visit('index.html')
  cy.get('body').should('have.css', 'background-color', 'rgb(0, 0, 0)')
})
First test confirms the application has black background after load

Tip:  instead of using RGB values to make the assertion, you can specify the expected value in different formats using chai-colors NPM module, see our recipe Adding Chai Assertions.

The above test is too short in my opinion. We want to go through the entire user action: visit the page, change the color using the input element, and observe the result. A more realistic test would be:

it('changes background color', () => {
  cy.visit('index.html')
  cy.get('body').should('have.css', 'background-color', 'rgb(0, 0, 0)')

  // select the new color value in the <input type="color">
  // element and trigger "change" event
  cy.get('input[type=color]')
    .invoke('val', '#ff0000')
    .trigger('change')

  // check the background color has been changed
  cy.get('body')
    .should('have.css', 'background-color', 'rgb(255, 0, 0)')
})

The test uses the .invoke command to set the value of the color input element, then calls the .trigger command to generate the "change" to the event. The application does the rest.

The test changes the background color

To better see how the .trigger command caused the application to change the color, hover over the command - Cypress shows before and after snapshots that include all styles.

Hover over trigger command to see how the app has changed in response

Spying on browser API

In the next test, let's confirm the implementation details. Our application calls the browser API method document.documentElement.style.setProperty to change the CSS variable. Because Cypress tests run directly in the same browser's window as the application, we can spy and stub DOM method calls directly.

it('can spy on native methods', () => {
  cy.visit('index.html')
  cy.get('body').should('have.css', 'background-color', 'rgb(0, 0, 0)')

  // create a spy and save it under an alias
  cy.document().its('documentElement.style')
    .then((style) => cy.spy(style, 'setProperty').as('setColor'))

  cy.get('input[type=color]')
    .invoke('val', '#ff0000')
    .trigger('change')

  cy.get('body').should('have.css', 'background-color', 'rgb(255, 0, 0)')
  // find spy by its alias and confirm it was called as expected
  cy.get('@setColor')
    .should('have.been.calledWith', '--background-color', '#ff0000')
})

The test passes, and Cypress Command Log shows the table with the spy, its alias, and how many times it was called. It also shows when it was called among the other Cypress commands.

"setColor" spy was called once right after triggering color change (green arrows)

Tip: if you do not care about values of some arguments in an assertion like

cy.get('@setColor')
  .should('have.been.calledWith', '--background-color', '#ff0000')

you can use Sinon.js placeholders. For example, skip the --background-color value and match any string using:

cy.get('@setColor')
  .should('have.been.calledWith', Cypress.sinon.match.string, '#ff0000')

See also

Read these blog posts that show more examples accessing the browser and DOM APIs directly from Cypress tests