Visit Non-HTML Page

How to make Cypress visit a JSON response for example.

Imagine you have a REST API endpoint that returns a JSON object. Can you see that JSON response in a Cypress test? In the blog post Test Plain Or Markdown File Using Cypress I have shown how to request a plain text resource using cy.request and write the received response into the empty application document using document.write. In this blog post I will show an alternative approach using cy.visit + cy.intercept commands.

🎁 You can find the code shown in this blog post in my Cypress Basics Workshop.

Let's say we set our backend with the data items from the fixture file. The initial code looks like this:

1
2
3
4
5
6
7
8
9
beforeEach(() => {
cy.fixture('two-items').as('todos')
})

beforeEach(function () {
// by using "function () {}" callback we can access
// the alias created in the previous hook using "this.<name>"
cy.task('resetData', { todos: this.todos })
})

Now we want to visit the /todos/1 resource to confirm the JSON is returned.

1
2
3
it('tries to visit JSON resource', () => {
cy.visit('/todos/1')
})

We get an error.

Trying to visit a JSON resource

Hmm, how do we "convince" Cypress that the received response should be treated as HTML text? By intercepting and overwriting the response content type header!

1
2
3
4
5
6
7
8
9
10
11
12
13
it('visits the todo JSON response', function () {
cy.intercept('GET', '/todos/*', (req) => {
req.continue((res) => {
if (res.headers['content-type'].includes('application/json')) {
res.headers['content-type'] = 'text/html'
}
req.body = `<body><pre>${res.body}</pre></body>`
})
}).as('todo')
cy.visit('/todos/1')
// make sure you intercept has worked
cy.wait('@todo')
})

The above works.

Treat JSON response as HTML

I like showing the response using this approach because it becomes visible in the test video, and can be captured using cy.screenshot command.

Let's confirm the title of the first todo is shown on the page. Because we have used function () { ... } syntax as the test callback, we can access the alias todos using this.todos inside the test.

1
2
3
4
5
cy.visit('/todos/1')
// make sure you intercept has worked
cy.wait('@todo')
// check the text shown in the browser
cy.contains(this.todos[0].title)

If you hover over the CONTAINS command, notice the found DOM element on the page is not highlighted.

The found element is not highlighted

This is because the response does not include the <body> element. Let's wrap our JSON response in some markup and make it prettier.

1
2
3
4
5
6
7
8
9
10
11
12
13
cy.intercept('GET', '/todos/*', (req) => {
req.continue((res) => {
if (res.headers['content-type'].includes('application/json')) {
res.headers['content-type'] = 'text/html'
const text = `<body><pre>${JSON.stringify(
res.body,
null,
2
)}</pre></body>`
res.send(text)
}
})
}).as('todo')

Now the element is highlighted correctly.

The found element is highlighted if we put a proper BODY markup

The last part I want to show is how to validate the URL using regular expression named captured groups. The URL should have the todo ID "1". We could split the pathname and get the id by index, but that is hard to maintain.

1
2
3
4
5
6
7
8
cy.location('pathname')
.should('include', '/todos/')
// we have a string, which we can split by '/'
.invoke('split', '/')
// and get the 3rd item in the array ["", "todos", "1"]
.its(2)
// and verify this is the same as the item ID
.should('eq', '1')

If the resources move from /todos/1 to /api/todos/1 finding all the test places where we get the ID part is going to be tricky. Instead let's use a regular expression to grab the ID via named capture group.

1
2
3
4
5
6
cy.location('pathname')
.should('include', 'todos')
// use named capture group to get the ID from the string
.invoke('match', /\/todos\/(?<id>\d+)/)
.its('groups.id')
.should('equal', '1')

Use a named capture group to extract the ID from the URL

Beautiful.