Emulate Media In Cypress Tests

How to set the "prefers-color-scheme" value in a Cypress test.

The application

Let's say our application has different styles depending on the media and the user's current prefers-color-scheme setting. In my example, the HTML page is normally uses the black text on the white background. If the user has prefers-color-scheme: dark setting, the page uses cyan on black colors to show the text.

🎁 You can find the source code for this blog post in the repo bahmutov/cypress-emulate-media.

public/index.html
1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<title>cypress-emulate-media</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<p class="text">
Lorem ipsum dolor sit amet, ...
</p>
</body>
</html>
public/style.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
body {
font-size: 2rem;
margin: 2rem 4rem;
}

/* default color theme */
.text {
background-color: #fff;
color: #333;
}

@media (prefers-color-scheme: dark) {
body {
background: #000;
}
.text {
background: #000;
color: #d0d;
font-weight: bold;
}
}

@media (prefers-color-scheme: light) {
.text {
background: white;
color: #555;
}
}

The page shown in the browser that prefers the light color scheme

Emulate color scheme using the Chrome DevTools

The simplest way to see how the page looks with the prefers-color-scheme: dark is to open the browser DevTools (I am using Chrome), and run the command "Emulate CSS prefers-color-scheme ..."

Pick the DevTools command to emulate the dark color theme

The page switches to use its dark media CSS styles

The page shown in the browser that prefers the dark color scheme

How do we control the CSS media from a Cypress test?

DevTools automation

Cypress has a built-in DevTools automation channel as I described in the Cypress Automation blog post. If you know that it is possible to execute a command from the DevTools, then you can find the actual command using the Chrome Debugger Protocol site. I have found the Emulation commands that has what we need.

The Emulation DevTools protocol commands

Emulation.setEmulatedMedia command

Let's use it in our test

cypress/e2e/dark.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it('prefers the dark color scheme', () => {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'dark',
},
],
},
}),
)

cy.visit('public/index.html')
})

Success!

The Cypress web test running using the dark color preference

Tip: I use cy.wrap(...) around the Promise-returning Cypress.automation call to make all other Cypress commands like cy.visit wait until the Emulation.setEmulatedMedia command has finished. See my cy.wrap examples.

Confirm the applied CSS

Let's make sure the actual dark background color is used by the page. We can grab the DOM element and ask the window object to give us the computed CSS property.

cypress/e2e/dark.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
it('prefers the dark color scheme', () => {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'dark',
},
],
},
}),
)

cy.visit('public/index.html')
cy.get('.text')
.then(($el) => window.getComputedStyle($el[0]).backgroundColor)
.then(cy.log)
.should('equal', 'rgb(0, 0, 0)') // black color!
})

The .then(($el) => window.getComputedStyle($el[0]).backgroundColor) code is a little unwieldy. Let's make a utility function to get us the computed style by name.

cypress/e2e/utils.js
1
2
3
4
// use Lodash _.camelCase to support both "backgroundColor" and "background-color"
const { camelCase } = Cypress._
export const getComputedProperty = (property) => ($el) =>
window.getComputedStyle($el[0])[camelCase(property)]
cypress/e2e/dark.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { getComputedProperty } from './utils'
it('prefers the dark color scheme', () => {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'dark',
},
],
},
}),
)

cy.visit('public/index.html')
cy.get('.text')
.then(getComputedProperty('background-color'))
.then(cy.log)
.should('equal', 'rgb(0, 0, 0)') // black color!
})

Nice.

Confirm the light color scheme

Let's have a spec that explicitly sets the light color theme preference and verifies the colors.

cypress/e2e/light.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { getComputedProperty } from './utils'

it('uses the light color scheme', () => {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'light',
},
],
},
}),
)

cy.visit('public/index.html')
cy.get('.text')
.then(getComputedProperty('background-color'))
.then(cy.log)
.should('equal', 'rgb(255, 255, 255)') // white!
cy.get('.text')
.then(getComputedProperty('color'))
.then(cy.log)
.should('equal', 'rgb(85, 85, 85)') // #333!
})

The Cypress web test running using the light color preference

Warning

Once you change the page media preferences, it stays that way. The Cypress Time-Traveling Debugger does NOT restore the media preferences when you hover over the command DOM snapshots. Thus if you have different tests in the same spec file, or switch the media preferences in the same test, it will not show the correct CSS styles when you inspect the commands.

cypress/e2e/light-and-dark.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { getComputedProperty } from './utils'

it('uses the light color scheme then the dark', () => {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'light',
},
],
},
}),
)

cy.visit('public/index.html')
cy.get('.text')
.then(getComputedProperty('background-color'))
.then(cy.log)
.should('equal', 'rgb(255, 255, 255)') // white!
cy.get('.text')
.then(getComputedProperty('color'))
.then(cy.log)
.should('equal', 'rgb(85, 85, 85)') // #333!
.wait(1000) // for demo
.then(() =>
Cypress.automation('remote:debugger:protocol', {
command: 'Emulation.setEmulatedMedia',
params: {
media: 'page',
features: [
{
name: 'prefers-color-scheme',
value: 'dark',
},
],
},
}),
)
cy.get('.text')
.then(getComputedProperty('background-color'))
.then(cy.log)
.should('equal', 'rgb(0, 0, 0)') // black color!
})

The test passes. But when I hover over the commands, the page CSS Media preference is not restored, and thus I see the last dark color them CSS.

The Cypress time-traveling debugger still uses the last media CSS

So just watch out for that.

Use cypress-cdp

You can simply Cypress automation commands and avoid the extra cy.wrap and cy.then(() => Cypress.automation(...)) code by using my plugin cypress-cdp. I will show how to use cy.CDP to emulate the color theme preference in my Cypress Plugins course.