Skip to content

Instantly share code, notes, and snippets.

@amirrustam
Created June 15, 2020 13:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save amirrustam/a9f843c64f5f333bd567a301fee601fb to your computer and use it in GitHub Desktop.
Save amirrustam/a9f843c64f5f333bd567a301fee601fb to your computer and use it in GitHub Desktop.
Handeling GQL

Via fetch spying

Let's start off by storing the GQL API endpoint as an environment variable for convenient retrieval within our tests.

// cypress.json

{
  "env": {
    "gqlEndpoint": "http://localhost:4000/graphql"
  }
}

To keep track of GQL API calls we can spy on specific fetch calls. This can be accomplished by spying on fetch calls that receive the desired arguments. For example, we can spy on calls with a specific operation name or query. There is a lot of flexibility here to spy with great granularity.

// commands.js

const { sinon } = Cypress
let fetchSpy

// Spy on global fetch before every page load
Cypress.on("window:before:load", (win) => {
  fetchSpy = cy.spy(win, "fetch")
})

Cypress.Commands.add("waitForGqlCall", (matchFn, callIteration = 1) => {
  const requestPayloadMatcher = sinon.match({
    method: sinon.match("POST"),
    body: sinon.match((reqBodyRaw) => {
      // operationName, variables, and query are passed into matchFn
      // and spies can be set based on these parameters
      return matchFn(JSON.parse(reqBodyRaw))
    })
  })

  return cy.wrap(fetchSpy.withArgs(Cypress.env("gqlEndpoint"), requestPayloadMatcher), { log: false })
    .should('have.callCount', callIteration)
    .its(`returnValues.${callIteration - 1}`, { log: false })
    .then((resp) => {
      if (!resp.ok) {
        throw new Error(`Request was not successful | Status: ${resp.status}`)
      }
    })
})

As shown in the above snippet we need to setup spying on fetch prior to loading the window, because fetch requests can start off immediately upon loading page, which can result on losing track of some requests.

In our test code we can utilize the waitForGqlCall custom command which does the following:

  • Accepts a matcher function (matchFn) that receives the GQL call parameters (operationName, variables, and query). The matchFn can return a "truthy" value to specify if a given fetch call should be spied on.
  • Utilizes Cypress's built-in retry-ability by ensuring the desired GQL call occurred and we can also assert that it occurred a given amount of times (the default is 1).
  • Once we've asserted that the desired GQL call was properly made, we can then ensure that its response was successful before proceeding with the rest of our test code.

For example, let's say we fill out a login form that makes a Login GQL operation upon submission. We can spy on the desired fetch call via a matchFn that checks if a call had an operationName that equals "Login":

// app.spec.js

const loginOpMatcher = ({ operationName }) => operationName === "Login"

describe("Awaiting Graphql", function () {

  beforeEach(() => {
    cy.visit("localhost:3000")
  })

  it("Await GQL request via spying", function () {
    cy.get('[data-testid=login-input]').type("amir@cypress.io")
    cy.get('form').submit()

    cy.waitForGqlCall(loginOpMatcher)
		
		// if you need to await the multiple instances of the same request 
    cy.waitForGqlCall(loginOpMatcher, 2)
    cy.waitForGqlCall(loginOpMatcher, 3)
  })
})

If you are batching GQL calls, waitForGqlCall can bed updated to support matching calls batched in an array:

Cypress.Commands.add("waitForGqlCall", (matchFn, callIteration = 1) => {
  const requestPayloadMatcher = sinon.match({
    method: sinon.match("POST"),
    body: sinon.match((reqBody) => {
      // operationName, variables, and query are passed into matchFn
      // and spies can be set based on these parameters
			return Array.isArray(reqBody)
				? reqBody.some((reqBodyRaw) => matchFn(JSON.parse(reqBodyRaw)))
				: matchFn(JSON.parse(reqBodyRaw)
    })
  })

  return cy.wrap(fetchSpy.withArgs(Cypress.env("gqlEndpoint"), requestPayloadMatcher), { log: false })
    .should('have.callCount', callIteration)
    .its(`returnValues.${callIteration - 1}`, { log: false })
    .then((resp) => {
      if (!resp.ok) {
        throw new Error(`Request was not successful | Status: ${resp.status}`)
      }
    })
})

If you don't know with certainty which batch a given query would be included in (i.e. is it batch with first call iteration or second), then you could simply iterate and assert all the iterations since fetchSpy keeps track of everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment