Check URL Search Params Using Cypress

Parse and confirm URL search params with retries.

This blog post shows how to validate the URL search parameters (the part of the URL after the question mark, like ?id=123&name=Gleb) from a Cypress test.

Example application

For this blog post I will use an online e-store application from the Testing The Swag Store online course. When the user adds an item to the cart, the URL changes to include the item's ID and count (both are pretend) in its search params like this ?count=1&itemId=....

Application sets the URL search parameters

Here is the starting code for this blog post: we simply want to log in and click the "Add to cart" button.

cypress/e2e/login/url-search-params.cy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { LoginPage } from '@support/pages/login.page'

const itemId = 4

beforeEach(() => {
cy.visit('/')
LoginPage.getUsername().type('standard_user')
LoginPage.getPassword().type('secret_sauce')
LoginPage.getLogin().click()
cy.location('pathname').should('equal', '/inventory')

cy.get(`.inventory_item[data-itemid="${itemId}"]`)
.contains('button', 'Add to cart')
.click()
})

it('controls the URL search params ...', () => { ... })

Let's validate the count and item values the application sets in the URL search params.

Check the URL search string

Cypress has two queries that return the URL information: cy.url and cy.location. I have seen people mostly using cy.url command, but I prefer using the cy.location. The cy.location returns any part of the parsed URL: the host, the full URL, the search part. In fact, cy.url is simply an alias to cy.location('href') command!

1
2
3
4
cy.url() // yields "https://acme.co/page/foo.html?count=...
cy.location('protocol') // yields "https"
cy.location('pathname]') // yields "/page/foo.html"
cy.location('search]') // yields "?count=..."

Without any arguments, cy.location yields an object with all URL parsed parts

1
2
3
cy.location().should(loc => {
// loc object has protocol, pathname, search and other properties
})

Ok, let's simply check the search property as a string.

1
2
3
it('controls the URL search params (check the search string)', () => {
cy.location('search').should('include', `count=1&item=${itemId}`)
})

The test checks the search URL property with retries because it follows the form QUERY . SHOULD(assertion).

The parameters order

In our application the order of search params is NOT guaranteed. Sometimes it is count=...&item=... and other times it is item=...&count=.... Thus our solution would fail sometimes. The test is flaky. We can improve it by listing the two possible orders.

1
2
3
4
5
6
it('controls the URL search params (check the search string)', () => {
cy.location('search').should('be.oneOf', [
`?count=1&item=${itemId}`,
`?item=${itemId}&count=1`,
])
})

be.oneOf assertion test

Great, but the assertion does not show the possible matches, and could fail if we have more parameters, or some unknown parameters that our test should not verify.

Multiple assertions

Let's avoid checking the entire URL search params string and check individual values instead. We are interested in the count=... and the item=... parameters, so let's attach multiple assertions to the same cy.location('search') query.

1
2
3
4
5
it('controls the URL search params (multiple assertions)', () => {
cy.location('search')
.should('include', 'count=1')
.and('include', `item=${itemId}`)
})

The test retries because it follows the form QUERY . SHOULD(assertion1) . AND(assertion2) .... The .should(...) and .and(...) assertion commands are equivalent and are used for better readability.

The passing test and the assertions

The separate assertions are much clearer in the Command Log.

Using URLSearchParams

Instead of checking substrings, we could let the browser parse and process the URL. There is a built-in standard for URL parsing that we can use via URLSearchParams object. We can create an instance of URLSearchParams and get the individual parameters as strings inside a should(callback) function:

1
2
3
4
5
6
7
it('controls the URL search params (parse URLSearchParams)', () => {
cy.location('search').should((search) => {
const params = new URLSearchParams(search)
expect(params.get('count')).to.equal('1')
expect(params.get('item')).to.equal(String(itemId))
})
})

Parsed URLSearchParams properties

If we want to verify all parameters irrespective of their order, we can grab all properties of the parsed search params object, make an object, and use deep.equal assertion to check them:

1
2
3
4
5
6
7
8
9
10
it('has only count and item search params', () => {
cy.location('search').should((search) => {
const params = new URLSearchParams(search)
const args = Object.fromEntries(params)
expect(args, 'params').to.deep.equal({
count: '1',
item: String(itemId),
})
})
})

Parsed URLSearchParams to plain object and deep.equal assertion

Tip: if you only know some parameters, you can use deep.include assertion instead of deep.equal.

In both cases, I prefer using URLSearchParams to checking strings. The URL parsing might be tricky if the parameters are encoded, so don't do it yourself.

URLSearchParams without should(callback)

We can improve the readability of the above test by breaking apart the single should(callback). If you looks at the assertion callback function it:

  • takes the current subject (a string) and constructs an instance of URLSearchParams
  • calls Object.fromEntries to make a plain object from URLSearchParams instance
  • checks the plain object using an assertion

We can rewrite the callback function into a series of queries that transform the subject (the URL search string) and run an assertion. Cypress lacks the necessary queries, but I can use my plugin cypress-map to fill the missing pieces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'cypress-map`

it('has only count and item search params (cypress-map)', () => {
cy.location('search')
.make(URLSearchParams)
.toPlainObject('entries')
.map({
count: Number,
item: Number,
})
.should('deep.equal', {
count: 1,
item: itemId,
})
})

Using cypress-map queries to transform the subject before asserting

A little explanation of each step in the above test:

  • cy.location('search') yields a string
  • .make(URLSearchParams) takes the string subject and constructs an object using new URLSearchParams(s)
  • .toPlainObject('entries') takes the object and yields Object.fromEntries(o)
  • we map strings in the current subject to numbers for exact comparison later using
    1
    2
    3
    4
    .map({
    count: Number,
    item: Number,
    })
  • the last assertion checks the properties of the current subject
    1
    2
    3
    4
    .should('deep.equal', {
    count: 1,
    item: itemId,
    })

If the assertion fails, it goes back all the way to cy.location('search') query and the steps run again. Beautiful.

🎓 This blog post gives examples from the lesson Bonus 64: Validate URL search params of the Testing The Swag Store online course. Often the URL parameters are URL encoded and thus you need to account for this in your assertions. See the lesson Bonus 65: confirm the escaped URL search params for hands-on exercises showing how to do it reliably.