Cypress Automation

Using Chrome Debugger Protocol from Cypress

When Cypress controls a Chromium browser, it has an open remote interface connection between Cypress and the browser. Typically, Cypress uses it to visit the site and perform special operations like setting cookies, or setting the file downloads folder. In this blog post I will show how to use Cypress.automation command to set the browser permission and to take native screenshot images.

Set the browser permission

This code example comes from the recipe in the cypress-example-recipes repo.

If we want to access the clipboard from the test, the browser asks the user for permission. The test can always query the current permission

1
2
3
4
5
6
7
8
9
10
11
12
13
it('can be queried in Chrome', { browser: 'chrome' }, () => {
cy.visit('index.html') // yields the window object
.its('navigator.permissions')
// permission names taken from
// https://w3c.github.io/permissions/#enumdef-permissionname
.invoke('query', { name: 'clipboard-read' })
// by default it is "prompt" which shows a popup asking
// the user if the site can have access to the clipboard
// if the user allows, then next time it will be "granted"
// If the user denies access to the clipboard, on the next
// run the state will be "denied"
.its('state').should('be.oneOf', ['prompt', 'granted', 'denied'])
})

If we look at the Chrome Debugger Protocol, we can see that there is a way to call a command to set the permission using Browser.setPermission command. By granting the test runner the permission, the browser skips showing the "should this site have access to the clipboard?" user prompt.

Browser.setPermission command

To call this command from the Cypress test, use Cypress.automation command:

1
2
3
4
5
6
7
8
9
10
11
// only the Chrome CDP is supported
// thus the first argument is always "remote:debugger:protocol"
Cypress.automation('remote:debugger:protocol', {
command: 'Browser.grantPermissions',
params: {
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
// make the permission tighter by allowing the current origin only
// like "http://localhost:56978"
origin: window.location.origin,
},
})

The promise-returning Cypress.automation command is very low-level, thus it is NOT automatically inserted into the Cypress test command chain. To make the test "wait" for the promise to resolve, use the cy.wrap command:

1
2
3
4
5
6
7
8
9
cy.wrap(Cypress.automation('remote:debugger:protocol', {
command: 'Browser.grantPermissions',
params: {
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
// make the permission tighter by allowing the current origin only
// like "http://localhost:56978"
origin: window.location.origin,
},
}))

Tip: if you want to run the automation command after other Cypress commands, make sure to return the the Cypress.automation(...) promise from the .then callback; we will see such example in the next section.

You can watch setting the browser permission in this video below:

Saving native screenshots

This example comes from the bahmutov/monalego repo.

If you want to save the application page without any visual artifacts introduced by cy.screenshot command, you can use the Page.captureScreenshot CDP command.

Page.captureScreenshot command

Let's say we want to capture the screenshot after a two second delay. We place the Cypress.automation inside a .then callback after the cy.wait command. The callback automatically waits for the returned promise to resolve.

1
2
3
4
5
6
7
8
9
10
11
12
13
cy.visit('/smile')
.wait(2000)
.then(() => {
cy.log('Page.captureScreenshot')
// https://chromedevtools.github.io/devtools-protocol/
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot
return Cypress.automation('remote:debugger:protocol', {
command: 'Page.captureScreenshot',
params: {
format: 'png',
},
})
})

The CDP documentation says the method returns an object with data property that is base64-encoded PNG image. We can grab this property and use cy.writeFile command to save the image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cy.visit('/smile')
.wait(2000)
.then(() => {
cy.log('Page.captureScreenshot')
// https://chromedevtools.github.io/devtools-protocol/
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot
return Cypress.automation('remote:debugger:protocol', {
command: 'Page.captureScreenshot',
params: {
format: 'png',
},
})
})
.its('data')
.then((base64) => {
cy.writeFile('test-smile.png', base64, 'base64')
})

The test runs and saves and image like this one:

Saved screenshot PNG image

If you want to capture only a portion of the page, grab the bounding box of an element and pass it as a parameter.

Limitation

As of Cypress v7, you can only execute a CDP automation command, not to subscribe to the browser events. If you need to subscribe, you would need to open your own remote interface connection, just like cypress-log-to-output does.

Yet, despite of this limitation right now, think what having an automation command in Cypress means - everyone that Puppeteer can do, Cypress can do too - it is the same Chrome Debugger Protocol connection after all! You want real click events? You want hover? You want a tab? No problem, see cypress-real-events for example.