Send With Confidence
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
Time to read: 15 minutes
cy.get(“[data-hook=’someSelector’]”)
or cy.find(“.selector”)
.cy.contains(“someText”)
or getting an element with a certain selector that contains some text such as cy.contains(“.selector”, “someText”)
.cy.get(“.selector”).within(() => { cy.get(“.child”) })
.cy.get(“tr”).each(($tableRow) => { cy.wrap($tableRow).find(‘td’).eq(1).should(“contain”, “someText” })
.cy.get(“.buttonFarBelow”).scrollIntoView()
.{ timeout: timeoutInMs }
like cy.get(“.someElement”, { timeout: 10000 })
.{ force: true }
property in those function calls to bypass some checks with the elements. This often occurs when an element is covered in some way or derived from an external library that you do not have much control over in terms of how it renders elements.
cy.get(“.button”).click()
.cy.get(“input”).type(“somekeyboardtyping”)
and we may need to clear out some default values of inputs by clearing it first like cy.get(“input”).clear().type(“somenewinput”)
. There are also cool ways to type other keys like {enter}
for the Enter key when you do cy.get(“input”).type(“text{enter}”)
.cy.get(“select”).select(“value”)
and checkboxes like cy.get(“.checkbox”).check()
.cy.get(“.selector”).should(“be.visible”)
and cy.get(“.selector”).should(“not.be.visible”)
.cy.get(“.element”).should(“exist”)
or cy.get(“.element”).should(“not.exist”)
.cy.get(“button”).should(“contain”, “someText”)
and cy.get(“button”).should(“not.contain”, “someText”)
.cy.get(“button”).should(“be.disabled”)
.cy.get(“.checkbox”).should(“be.checked”)
.cy.get(“element”).should(“have.class”, “class-name”)
. There are other similar ways to test attributes as well with .should(“have.attr”, “attribute”)
.cy.get(“div”).should(“be.visible”).and(“contain”, “text”)
.cy.request(...)
to make HTTP requests to your backend endpoints with auth headers. Another alternative is you can build out cy.task(...)
plugins that can be called from any spec file to cover other functionality that can be best handled in a Node server with other libraries such as connecting to an email inbox and finding a matching email or having more control over the responses and polling of certain API calls before returning back some values for the tests to use.
cy.request()
to make HTTP requests to your backend API to set up or tear down data before your test cases run. You usually pass in the endpoint URL, HTTP method such as “GET” or “POST”, headers, and sometimes a request body to send to the backend API. You can then chain this with a .then((response) => { })
to gain access to the network response through properties such as “status” and “body”. An example of making a cy.request()
call is demonstrated here.
cy.request(...)
will fail with a 4xx or 5xx status code during the clean up before a test runs. One scenario where you may choose to ignore the failing status code is when your test makes a GET request to check whether an item still exists and was already deleted. The item may already be cleaned up and the GET request will fail with a 404 not found status code. In this case, you would set another option of failOnStatusCode: false
so your Cypress tests do not fail before even running the test steps.
cy.task()
plugin for it. We create plugin functions in module files and import them in the plugins/index.ts
where we define the task plugins with the arguments we need to run the functions as shown below.
cy.task(“pluginName”, { ...args })
anywhere in your spec files and you can expect the same functionality to happen. Whereas, if you used cy.request()
, you have less reusability unless you wrapped those calls themselves in page objects or helper files to be imported everywhere.
Cypress.env(“apiHost”)
or cy.getCookie(‘auth_token’)
. You pass in things such as the auth token string or backend API host to your plugin function’s argument object in addition to things required for the request body if it needs to talk to your backend API.
cy.server()
and cy.route()
to listen for routes to mock out responses for any state you want. Here’s an example:
cy.server()
, cy.route()
, and cy.wait()
together to listen and wait for network requests to finish before doing next steps. Usually, after loading a page or doing some sort of action on the page, an intuitive visual cue will signal that something is complete or ready for us to assert and act on. For the cases where you don’t have such a visible cue, you can explicitly wait for an API call to finish like this.
window.fetch
with an XHR polyfill and doing some setup and cleanup steps before your tests run as recorded in these issues. There is also an experimentalFetchPolyfill
property as of Cypress 4.9.0 that may work for you, but overall, we are still looking for better methods to handle network stubbing across fetch and XHR usage in our applications without things breaking. As of Cypress 5.1.0, there is a promising new cy.route2()
function (see the Cypress docs) for experimental network stubbing of both XHR and fetch requests, so we plan to upgrade our Cypress version and experiment with it to see if it solves our issues.
support/commands.ts
, you can access the functions like cy.customCommand()
or cy.login()
. Writing up a custom command for logging in looks like this.
open()
for inherited page classes to share and extend from. Derived page classes define their own getter functions for selectors and other helper functions while reusing the base classes’ functionality through calls like super.open()
as shown here.
window.Cypress
check.
window.Cypress
property added to the browser. In your client-side code, you can choose to check if there is no Cypress property on the window object, then carry out the download as usual. But, if it’s being run in a Cypress test, do not actually download the file. We also took advantage of checking the window.Cypress
property for our A/B experiments running in our web app. We did not want to add more flakiness and non-deterministic behavior from A/B experiments potentially showing different experiences to our test users, so we first checked the property is not present before running the experiment logic as highlighted below.
cy.iframe(iframeSelector)
custom command to encapsulate dealing with iframes. Passing in a selector to the iframe will then check the iframe’s body contents until it is no longer empty and then return back the body contents for it to be chained with more Cypress commands as shown below:
index.d.ts
file:
cy.find(...).type(...)
and other alternatives not working, but thankfully we found a workaround by changing the values of the inputs and selects directly with the invoke command i.e. cy.get(selector).invoke(‘val’, ‘some value’)
. You’ll also need ”chromeWebSecurity”: false
in your cypress.json
configuration file to allow you to bypass any cross-origin errors. A sample snippet of its usage with filler selectors is provided below:
Cypress.config(“baseUrl”)
to match those URLs such as https://staging.app.com or https://testing.app.com. This changes up the base URL for all of your cy.visit(...)
calls to append their paths to. There are multiple ways to set this such as setting CYPRESS_BASE_URL=<frontend_url>
before running your Cypress command or setting --config baseUrl=<frontend_url>
.
Cypress.env(“apiHost”)
. These will be used for your cy.request(...)
calls to make HTTP requests to certain paths like “<apiHost>/some/endpoint” or passed through to your cy.task(...)
function calls as another argument property to know which backend to hit. These authenticated calls would also need to know the auth token you most likely are storing in localStorage or a cookie through cy.getCookie(“auth_token”)
. Make sure this auth token is eventually passed in as part of the “Authorization” header or through some other means as part of your request. There are a multitude of ways to set these environment variables such as directly in the cypress.json
file or in --env
command-line options where you can reference them in the Cypress documentation.
fixtures
folder and import them in for you to use based on the “testEnv” value such as cy.fixture(`${testEnv}.json`).then(...)
. However, you cannot type out the JSON files well and there is much more room for mistakes in syntax and in writing out all the properties required per test. The JSON files are also farther away from the test code, so you would have to manage at least two files when editing the tests. Similar maintenance issues would occur if all the environment test data were set in environment variables directly in your cypress.json
and there would be too many to manage across a plethora of tests.
Cypress.env(“testEnv”)
to see which test environment you are running against and use that value to extract out the corresponding environment’s test fixture from the overall test fixture object and use those values in your test. The general idea of the test fixtures object is summarized in the code snippet underneath.
package.json
. You can also set up multiple JSON files for each environment’s configuration, but for simplicity, you will see the commands with the options and values inline.
package.json
scripts that your frontend “baseUrl” ranges from “http://localhost:9001” for when you start up your app locally to the deployed application URL such as “https://staging.app.com”. You can set the backend “apiHost” and “testEnv” variables to help with making requests to a backend endpoint and loading up a specific test fixture object. You may also create special “cicd” commands for when you need to run your tests in a Docker container with the recording key.
cy.get()
, cy.contains()
, .click()
, .type()
, .should(‘be.visible’)
.
cy.request()
, run arbitrary code in a Node server with cy.task()
, and stub out network requests using cy.server()
and cy.route()
. You can even create your own custom command like cy.login()
to help you log in to a user through the API. All these things help to reset a user to the proper starting point before tests run. Wrap these selectors and functions altogether in a file and you’ve created reusable page objects to use in your specs.
npm run cypress:open:staging
in your package.json
will load up the proper environment variable values and run the tests for the environment you chose to run against.
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
cy.request({ | |
method: 'POST', | |
// Can form the API host part based on environment variables but we show the full value for example purposes | |
url: `https://api.staging.com/v3/api_keys`, | |
headers: { | |
'Content-Type': 'application/json', | |
Accept: 'application/json', | |
// Pass through an Authorization header to tie this call to the logged in user of the Cypress test | |
Authorization: "token authtoken123", | |
}, | |
// If you don't care about whether or not this fails with a 4xx/5xx status code | |
// failOnStatusCode: false, | |
body: { | |
name: "api key name", | |
scopes: [], | |
} | |
}) | |
.then((response:) => { | |
const createdApiKey = response.body; | |
// ...assertions on response body and status | |
// So you can chain it even more after | |
return cy.wrap(createdApiKey); | |
}); |
// Alerts/setup.ts | |
// Sample AlertsSetup module | |
import nodefetch from 'node-fetch'; | |
const AlertsSetup = () => { | |
const getAlerts = ({ | |
token, | |
apiHost, | |
}: { | |
token: string; | |
apiHost: Backends; | |
}) => { | |
const endpoint = `${apiHost}${v3Alerts}`; | |
return nodefetch(endpoint, { | |
headers: { | |
Authorization: `token ${token}`, | |
// ... more headers | |
}, | |
method: 'GET', | |
}) | |
.then((response: Response) => { | |
if (response.ok) { | |
return response.json(); | |
} | |
throw new Error("Failed to retrieve alerts!"); | |
}) | |
.catch((error: Error) => { | |
console.error(error); | |
return []; | |
}); | |
} | |
return { | |
getAlerts, | |
// Other task functions for Alert | |
}; | |
}; | |
// plugins/index.ts | |
import AlertsSetup from './Alerts/setup'; | |
// More module imports of plugin functions | |
export default ( | |
on: Cypress.PluginEvents, | |
config: Cypress.PluginConfigOptions | |
) => { | |
// Usage: cy.task("taskFunctionName", { ...taskArgs }) | |
// Add more plugins here. This is how you can run arbitrary code i.e. API calls with more control, | |
// connecting to IMAP client, etc. on the Node side | |
// Make sure your plugins are returning promises that will resolve with certain values | |
on('task', { | |
// ...more task functions | |
getAlerts: ({ token, apiHost }: { token: string; apiHost: Backends }) => | |
// This encapsulates a node-fetch call to retrieve Alerts and return custom responses | |
// We pass in the auth token from cy.getCookie("auth_token") and API host from Cypress.env("apiHost") | |
// and potentially other request body arguments since we cannot do those calls in the Node server side | |
AlertsSetup.getAlerts({ | |
token, | |
apiHost, | |
}), | |
}); | |
return config; | |
}); |
cy.server(); | |
// Stub out PUT request matching the URL path to respond with certain values | |
// This will work for XHR-based requests out of the box, but needs a workaround for fetch requests | |
cy.route({ | |
method: 'PUT', | |
url: 'some/endpoint', | |
response: { | |
user_id: 1231235412, | |
username: "username", | |
}, | |
}); | |
// ...Trigger action in the UI that will lead to a PUT request and assert on things that change |
// Listen for XHR requests to finish, say if we don't have anything visible/reactive in the UI to say a request finished | |
cy.server(); | |
cy.route('GET', '**/v3/api_keys').as('getAPIKeys'); | |
// ...Trigger action that will do a GET request to /v3/api_keys | |
// Wait for API Keys request to finish | |
cy.wait('@getAPIKeys', { timeout: 10000 }); |
// Usage looks like cy.login("username", "password").then(...) | |
Cypress.Commands.add('login', (username, password) => { | |
const loginEndpoint = "/login"; | |
const apiHost = Cypress.env("apiHost"); | |
/* | |
Logging in through the API to get the auth token | |
We return the token so we can chain it like | |
`cy.login("username", "password").then((token) => { | |
cy.setCookie("auth_token", token); | |
}); | |
*/ | |
return cy.request({ | |
method: 'POST', | |
url: `${apiHost}${loginEndpoint}`, | |
body: { | |
username, | |
password, | |
} | |
}) | |
.then((response) => response.body.token); | |
}); |
export default class Page { | |
public timeout = 90000; | |
public open(path: string, options: any = {}) { | |
// You can later chain this with .then if you'd like | |
return cy.visit(`/${path}`, { timeout: this.timeout, ...options }); | |
} | |
// ...other shared functionality across all Pages | |
} |
import Page from '../../page'; | |
class SomePage extends Page { | |
get addButton() { | |
return cy.get("[data-testid=addButton]"); | |
} | |
// ...other getter selectors | |
open(options = {}) { | |
return super.open('some/page/route', options); | |
} | |
addItem(item: string) { | |
// ...this.doSomeComplicatedSteps(); | |
// ...this.fillOutForm(); | |
return this.addButton().click(); | |
// ...validateItemIsInList after chaining this function with .then | |
} | |
// ...other helper functions | |
} | |
export default new SomePage(); |
// As an extra escape hatch, you can choose to run or not run certain client-side code during Cypress tests by checking | |
// for the window.Cypress value. We highlight some example use cases for why you'd need to do this below | |
// If we're running an A/B experiment where different views may show up for a user, | |
// we need to make sure it doesn't lead to Cypress test flakiness by choosing not to run A/B tests during Cypress tests | |
if (!window.Cypress) { | |
runABExperiment(); | |
} | |
// If one of our flows deals with an auto-downloading link which may lead to breaking/pausing Cypress tests | |
// due to the file download, we can choose to focus on the success state showing up without actually downloading the file | |
// during Cypress tests | |
const autoDownloadLink = () => { | |
if (window.Cypress) { | |
// Skip actually downloading the file in Cypress tests | |
return; | |
} | |
// Outside of Cypress tests, auto download the file... | |
}; |
/** | |
* Cypress does not natively support iframes. Given the iframeSelector, | |
* this will resolve with the iframe's wrapped body element when it's finished loading. | |
* See https://github.com/cypress-io/cypress/issues/136 | |
* @param iframeSelector {string} iframe selector i.e. iframe#iframe-id | |
* @returns {Promise} - returns iframe's wrapped body element to chain off of | |
*/ | |
Cypress.Commands.add('iframe', (iframeSelector) => | |
// Get the iframe > document > body | |
// and retry until the body element is not empty and is loaded | |
cy | |
.get(iframeSelector) | |
.its('0.contentDocument.body') | |
.should('not.be.empty', { timeout: 15000 }) | |
// Wraps "body" DOM element to allow chaining more Cypress commands like ".find(...)" | |
.then((body) => cy.wrap(body)) | |
); |
declare namespace Cypress { | |
interface Chainable<Subject> { | |
// Typing out all of our custom commands i.e. cy.login(...) | |
/** | |
* Retrieve iframe's body content after it has loaded for us to chain like .find() or .within() after it | |
* @example | |
* cy.iframe(iframeSelector).within(() => { ... }) | |
*/ | |
iframe(iframeSelector: string): Chainable<Subject>; | |
// Typing out all of our custom plugins i.e. cy.task(...) | |
} | |
} |
cy.iframe("#paymentiframe").within(() => { | |
// Fill out credit card information | |
cy.get("#creditCardHolderName").invoke('val', 'test user'); | |
cy.get("#creditCardNumber").invoke('val', '4111111111111111'); | |
cy.get("#creditCardExpirationMonth").invoke('val', '05'); | |
cy.get("#creditCardExpirationYear").invoke('val', '2040'); | |
cy.get("#creditCardSecurityCode").invoke('val', '321'); | |
// Fill out credit card address details | |
cy.get("#creditCardAddress1").invoke('val', '123 SendGrid St'); | |
cy.get("#creditCardCountry").invoke('val', 'JPN'); | |
cy.get("#creditCardCity").invoke('val', 'Irvine'); | |
cy.get("#creditCardState").invoke('val', 'California'); | |
cy.get("#creditCardPostalCode").invoke('val', '90210'); | |
// Submit Zuora payment info | |
cy.get("#submitButton").click(); | |
}); |
// (Optional) Say we had a getTestEnv utility function that retrieves the "testEnv" environment variable value and defaults | |
// it some value in case the environment variable is not set and is undefined | |
const getTestEnv = () => { | |
return Cypress.env("testEnv") || "staging"; | |
}; | |
// Could be "testing" or "staging" but depends on how many environments you are willing to support | |
const testEnv = getTestEnv(); | |
const testFixtures = { | |
testing: { | |
username: "testingusername", | |
password: "testing123", | |
meta: { | |
domainName: "some_testing_domain", | |
// More metadata with values specific to testing environment | |
}, | |
}, | |
staging: { | |
username: "stagingusername", | |
password: "testing123", | |
meta: { | |
domainName: "some_staging_domain", | |
// More metadata with values specific to staging environment | |
}, | |
}, | |
}; | |
// Extract the environment's test fixture object i.e. the staging or testing inner object | |
const testFixture = testFixtures[testEnv]; | |
// We can use the different test fixture values with the same test logic | |
cy.login(testFixture.username, testFixture.password) | |
.then((token) => { | |
cy.setCookie("auth_token", token); | |
}); |
// Can import these utility functions from another file | |
const getTestEnv = () => Cypress.env("testEnv") || "staging"; | |
const getApiHost = () => Cypress.env("apiHost") || "https://staging.api.com"; | |
context("Some Page Test", () => { | |
const testFixtures = { | |
testing: { | |
username: "testingUsername", | |
password: "testingPassword", | |
meta: { | |
// Other metadata for testing environment | |
domainName: "testingDomainName" | |
}, | |
}, | |
staging: { | |
username: "stagingUsername", | |
password: "stagingPassword", | |
meta: { | |
// Other metadata for staging environment | |
domainName: "stagingDomainName | |
}, | |
}, | |
}; | |
const testFixture = testFixture[getTestEnv()]; | |
before(() => { | |
const { username, password } = testFixture; | |
cy.login(username, password).then((token) => { | |
cy.setCookie("auth_token", token); | |
// In a Cypress task plugin or a cy.request() call that needs to know the API host, auth token, | |
// and potentially other metadata, we pass them through | |
return cy.task("makeRequestToBackend", { | |
token, | |
apiHost: getApiHost(), | |
domain: testFixture.meta.domain, | |
}).then((response) => { | |
// ...do stuff based on the result | |
}); | |
}); | |
}); | |
beforeEach(() => { | |
// Keep the user logged in by preserving the auth token cookie | |
Cypress.Cookies.preserveOnce("auth_token"); | |
// The "baseUrl" config value affects our page object's `cy.visit("/some/page")` calls inside the open() function | |
// by setting a "baseUrl" like "https://staging.app.com" before appending the path i.e. "https://staging.app.com/some/page" | |
SomePage.open(); | |
}); | |
it("should be able to do something on the page", () => { | |
SomePage.someButton.click(); | |
SomePage.someModal.should("be.visible"); | |
// More actions/assertions on the page | |
// We may need to use testFixture.meta.<someProperty> to help us out here | |
}); | |
}); |
{ | |
"scripts": { | |
"cypress:open:localhost:testing": "cypress open --config baseUrl=http://localhost:9001 --env testEnv=testing,apiHost=https://testing.api.com", | |
"cypress:open:testing": "cypress open --config baseUrl=https://testing.app.com --env testEnv=testing,apiHost=https://testing.api.com", | |
"cypress:open:localhost:staging": "cypress open --config baseUrl=http://localhost:9001 --env testEnv=staging,apiHost=https://staging.api.com", | |
"cypress:open:staging": "cypress open --config baseUrl=https://staging.app.com --env testEnv=staging,apiHost=https://staging.api.com", | |
"cypress:run:localhost:testing": "cypress run --config baseUrl=http://localhost:9001 --env testEnv=testing,apiHost=https://testing.api.com", | |
"cypress:run:localhost:staging": "cypress run --config baseUrl=http://localhost:9001 --env testEnv=staging,apiHost=https://staging.api.com", | |
"cypress:run:testing": "cypress run --config baseUrl=https://testing.app.com --env testEnv=testing,apiHost=https://testing.api.com", | |
"cypress:run:staging": "cypress run --config baseUrl=https://staging.app.com --env testEnv=staging,apiHost=https://staging.api.com", | |
"cypress:run:cicd:testing": "cypress run --record --key <record_key> --config baseUrl=https://testing.app.com --env testEnv=testing,apiHost=https://testing.api.com", | |
"cypress:run:cicd:staging": "cypress run --record --key <record_key> --config baseUrl=https://staging.app.com --env testEnv=staging,apiHost=https://staging.api.com" | |
} | |
} |