Send With Confidence
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
Time to read: 9 minutes
cypress.json
file is where you can set all the configuration for your Cypress tests such as base timeouts for your Cypress commands, environment variables, and other properties.
{ timeout: timeoutInMs }
throughout your Cypress commands. Tinker with the numbers until you find the right balance for settings such as “defaultCommandTimeout,” “requestTimeout,” and “responseTimeout.”cy.task(‘someTaskPluginFunction)
, Cypress.env(‘someEnvVariable’)
, or cy.customCommand()
).
cypress.json
file and loading up a separate Cypress configuration file for each test environment, such as a staging.json
when you run different Cypress scripts in your package.json
. The Cypress documentation does a great job of walking you through this approach.
cypress
folder with a guided structure when you first start up Cypress in your codebase. This includes other folders such as integration
for your spec files, fixtures
for JSON data files, plugins
for your cy.task(...)
functions and other configuration, and a support
folder for your custom commands and types.
pages
folder partitioned by feature name such as “SenderAuthentication” to hold all the page objects underneath the /settings/sender_auth
route such as “DomainAuthentication” (/settings/sender_auth/domains/**/*) and “LinkBranding” (/settings/sender_auth/links/**/*). In our plugins
folder, we also did the same thing with organizing all the cy.task(...)
plugin files for a certain feature or page in the same folder for Sender Authentication, and we followed the same approach for our integration
folder for the spec files. We’ve scaled our tests to hundreds of spec files, page objects, plugins, and other files in one of our codebases—and it’s easy and convenient to navigate through.
cypress
folder.
integration
folder (where all your specs live) is potentially splitting up spec files based on the test’s priority. For example, you may have all the highest priority and value tests in a “P1” folder and the second priority tests in a “P2” folder so you can easily run all the “P1” tests by setting the --spec
option like --spec ‘cypress/integration/P1/**/*’
.
--spec ‘cypress/integration/SomePage/**/*’
, but also by some other criteria like priority, product, or environment.
hooks.ts
file that manages and exports an object with a readable key name for the element and the actual string CSS selector for the element as the value. We will call these the “Read Selectors” as we need to read and use the CSS selector form for an element in calls such as Enzyme’s wrapper.find(“[data-hook=’selector’]”)
in our unit tests or Cypress’s cy.get(“[data-hook=’selector’]”)
in our page objects. These calls would then look cleaner like wrapper.find(Selectors.someElement)
or cy.get(Selectors.someElement)
.
{ “data-hook”: “selector” }
as the value. We will call these the “Write Selectors” as we need to write or spread these objects onto a React component as props to successfully add the “data-hook” attribute to the underlying elements. The goal would be to do something like this
and the actual DOM element underneath—assuming props are passed down to the JSX elements correctly—will also have the “data-hook=” attribute set.
[data-hook=”selector”]
CSS selector string for each element name. If a key’s corresponding value is null
, we’ll assume the element name in the input object will be the same as the “data-hook” value such as { someElement: null } => { someElement: ‘[data-hook=”someElement”] }
. Otherwise, if a key’s corresponding value is not null, we can choose to override that “data-hook” value such as { someElement: “newSelector” } => { someElement: ‘[data-hook=”newSelector”]’ }
.
{ “data-hook”: “selector” }
objects for each element name. If a key’s corresponding value is null
, we’ll assume the element name in the input object will be the same as the “data-hook” value such as { someElement: null } => { someElement: { “data-hook”: “someElement” } }
. Otherwise, if a key’s corresponding value is not null
, we can choose to override that “data-hook” value such as { someElement: “newSelector” } => { someElement: { “data-hook”: “newSelector” }’ }
.
Selectors.unknownElement
or WriteSelectors.unknownElement
in our TypeScript files as well. Before exporting our read selectors, we also allow adding extra element and CSS selector mappings for third-party components that we do not have control over. In some cases, we cannot add a “data-hook” attribute to certain elements, so we still need to select by other attributes, ids, and classes and add more properties to the read selector object as shown below.
cypress.json
such as “defaultCommandTimeout” and “responseTimeout”. We would go back and adjust certain pages that needed longer timeouts, but then over time, the number of arbitrary timeout values grew and maintaining it became harder for large scale changes.
cy.get(...)
or cy.contains(...)
. Beyond that default timeout, we would scale up to “short”, “medium”, “long”, “xlong”, and “xxlong” within a timeout object we can import anywhere in our files to use in our Cypress commands such as cy.get(“someElement”, { timeout: timeouts.short })
or cy.task(‘pluginName’, {}, { timeout: timeouts.xlong })
. After replacing our timeouts with these values from our imported object, we have one place to update to scale up or scale down the time it takes for certain timeouts.
// timeouts.ts | |
// Consolidate your timeout values with something that scales so you don't end up with a bunch of arbitrary timeout values | |
// for separate selectors, requests, tasks, etc. for one off cases | |
// Your `cypress.json` configuration should have reasonable timeouts to cover most timeout cases though | |
export const timeouts = { | |
short: 15000, | |
medium: 30000, | |
long: 60000, | |
xlong: 120000, | |
xxlong: 240000, | |
}; | |
// In some .spec.ts | |
cy.get("[data-hook='test']", { timeout: timeouts.short }); | |
cy.task("someTask", {}, { timeout: timeouts.xlong }); |
Partner with the email service trusted by developers and marketers for time-savings, scalability, and delivery expertise.
{ | |
"baseUrl": "http://localhost:9001", | |
"chromeWebSecurity": false, | |
"projectId": "projectidhash", | |
"blacklistHosts": [ | |
"*googletagmanager.com", | |
"*google-analytics.com", | |
"*moreanalytics.com", | |
"*moreblockedscripts.com" | |
], | |
"defaultCommandTimeout": 10000, | |
"responseTimeout": 60000, | |
"viewportHeight": 1024, | |
"viewportWidth": 1280, | |
"videoUploadOnPasses": false, | |
"numTestsKeptInMemory": 10 | |
} |
// cypress/ | |
// |-- fixtures/ (some data JSON files here) | |
// |-- example.json | |
// |-- integration/ (spec files live here broken down by feature/page) | |
// |-- SenderAuthentication/ | |
// |-- DomainAuthentication/ | |
// |-- domain_create.spec.ts | |
// |-- domain_delete.spec.ts | |
// |-- LinkBranding/ | |
// |-- ...spec.ts | |
// |-- SomePage/ | |
// |-- some_healthchecks.spec.ts | |
// |-- some_flow.spec.ts | |
// |-- SomeOtherPage/ | |
// |-- ...spec.ts | |
// |-- pages/ (page objects for different features/pages here) | |
// |-- SenderAuthentication/ | |
// |-- DomainAuthentication/ | |
// |-- domain_listing_page.ts | |
// |-- domain_create_page.ts | |
// |-- domain_create_step2_page.ts | |
// |-- LinkBranding/ | |
// |-- ...page.ts | |
// |-- SomePage/ | |
// |-- some_page.ts | |
// |-- plugins/ | |
// |-- index.ts (task functions defined here) | |
// |-- SenderAuthentication/ | |
// |-- setup.ts (task functions for Sender Authentication) | |
// |-- SomePage/ | |
// |-- ...some_setup.ts | |
// |-- support/ | |
// |-- commands.ts (some custom commands defined here) | |
// |-- index.ts (where we import all of our custom commands, define other Cypress functions) | |
// |-- index.d.ts (where we type all of our tasks, custom commands, etc.) | |
// |-- utils/ | |
// |-- testEnv.ts | |
// |-- timeouts.ts | |
// |-- otherUtils.ts |
const Selectors = { | |
addButton: “[data-hook=’addButton’]” | |
// More read selectors | |
}; | |
// cypress page objects | |
cy.get(Selectors.addButton); | |
// jest/enzyme unit tests | |
wrapper.find(Selectors.addButton); |
// Placing "data-hook" attribute directly onto JSX elements | |
<div data-hook=”important-element”>Some important element</div> | |
// or spreading an object with "data-hook" onto JSX elements | |
<div {...{ “data-hook”: “important-element” }}>Some important element</div> | |
// Spreading the data-hook objects onto Components and eventually spread onto an | |
// important parent element through restOfProps | |
<Component {...{ “data-hook”: “important-element” }}>blah</Component> | |
const Component = (...restOfProps) => { | |
return <div {...restOfProps}>Some important element</div> | |
} | |
const WriteSelectors = { | |
addButton: { "data-hook": “addButton” }, | |
// More write selectors | |
}; | |
// Spread Write Selectors onto React components like | |
<Component {...WriteSelectors.addButton} /> |
const selectHook = (hook: string): string => `[data-hook="${hook}"]`; | |
type Selectors<T> = { [P in keyof T]: string }; | |
/* | |
This takes in a general object like | |
{ | |
addButton: null, | |
someElement: "someNewName" | |
} | |
and turns it into this typed read selector object | |
{ | |
addButton: '[data-hook="addButton"]', | |
someElement: '[data-hook="someNewName"]' | |
} | |
*/ | |
export const readSelectorGenerator = < | |
T extends Record<string, string | null> | |
>( | |
selectors: T | |
) => | |
// CSS selector version of the the data-hook attributes intended to be used in Cypress page objects and unit tests | |
// i.e. { addButton: '[data-hook="addButton"]', ... } | |
// Usage: | |
// cy.get(Selectors.addButton) in Cypress page objects/specs | |
// wrapper.find(Selectors.addButton) in Jest/Enzyme unit tests | |
Object.keys(selectors) | |
.map((key) => ({ | |
[key]: | |
selectors[key] === null | |
? // When null, we assume the selector key and data-hook will have the same name | |
// i.e. { someSelector: null } => { someSelector: '[data-hook="someSelector"]' } | |
selectHook(key) | |
: // When selectors[key] has some string value, we assume the intent is to override the name of the data-hook attribute | |
// i.e. { someSelector: 'otherName' } => { someSelector: '[data-hook="otherName"]' } | |
selectHook(selectors[key] as string), | |
})) | |
.reduce((accumulator, nextValue) => ({ | |
...accumulator, | |
...nextValue, | |
})) as Selectors<T>; // Typing this as Selectors<T> allows us to get errors for Selectors.unknown and auto-completion for properties that exist |
interface WriteHook { | |
'data-hook': string; | |
} | |
const writeHook = (hook: string): WriteHook => ({ 'data-hook': hook }); | |
type WriteSelectors<T> = { [P in keyof T]: WriteHook }; | |
/* | |
This takes in a general object like | |
{ | |
addButton: null, | |
someElement: "someNewName" | |
} | |
and turns it into this typed read selector object | |
{ | |
addButton: { "data-hook": "addButton" }, | |
someElement: { "data-hook": "someNewName" } | |
} | |
*/ | |
export const writeSelectorGenerator = <T extends Record<string, string | null>>( | |
selectors: T | |
) => | |
// Adds objects with data-hook attributes intended to be passed/spread into React component props | |
// i.e. { buttonSelector: { "data-hook": "buttonSelector" }, ... } | |
// Usage: | |
// <ReactComponent {...WriteSelectors.buttonSelector} /> spreads the buttonSelector's 'data-hook' object selector onto your component | |
Object.keys(selectors) | |
.map((key) => { | |
return { | |
[key]: | |
selectors[key] === null | |
? // When null, we assume the selector key and data-hook will have the same name | |
// i.e. { someSelector: null } => { someSelector: { 'data-hook': 'someSelector' } } | |
writeHook(key) | |
: // When selectors[key] has some string value, we assume the intent is to override the name of the data-hook attribute value | |
// i.e. { someSelector: 'otherName' } => { someSelector: { 'data-hook': 'otherName' } } | |
writeHook(selectors[key] as string), | |
}; | |
}) | |
.reduce((accumulator, nextValue) => ({ | |
...accumulator, | |
...nextValue, | |
})) as WriteSelectors<T>; | |
// Typing this as WriteSelectors<T> allows us to get errors for WriteSelectors.unknown and auto-completion for properties that exist | |
// selectors.ts at the base of each page's component folder | |
import { writeSelectorGenerator, readSelectorGenerator } from ‘../path/to/selectorGenerator’; | |
const selectors = { | |
addButton: null, | |
submitButton: “superSubmitButton” | |
}; | |
export const Selectors = { | |
...readSelectorGenerator(selectors), | |
// For other CSS selectors that we could not spread data-hook attributes on or control the output of | |
dateRangePickerOption: “.date-range-picker option”, | |
}; | |
export const WriteSelectors = writeSelectorGenerator(selectors); | |
// some_page.ts - Cypress page object after importing the page’s specific selectors file | |
class SomePage extends Page { | |
get addButton() { | |
return cy.get(Selectors.addButton); | |
} | |
get submitButton() { | |
return cy.get(Selectors.submitButton); | |
} | |
// ... | |
} | |
// Importing the Write Selectors to spread onto React components | |
<button {...WriteSelectors.addButton}>Add</button> | |
// Importing Read Selectors to spread into Enzyme query functions | |
wrapper.find(Selectors.addButton); |