Many tools allow one config file to extend another one. For example, ESLint allows one to apply recommended settings and then add several more rules:
{
"plugins": [
"react"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"rules": {
"react/no-set-state": "off"
}
}
Similarly, the TypeScript compiler allows you to extend the configuration from the base file using the extends
keyword. If we place most of the settings in configs/base.json
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
Then we can write tsconfig.json
extending the base config:
{
"extends": "./configs/base",
"files": ["main.ts", "supplemental.ts"]
}
Cypress does not support extends
syntax in its configuration file. If you want to apply different settings, you need to write a complete second configuration file and use it via the --config-file <filename>
command line argument.
For example, when working with tests locally, we might use the default cypress.json
file:
{
"baseUrl": "http://localhost:8080",
"defaultCommandTimeout": 2000,
"video": true
}
When running tests against staging server, we might use a few different settings placed in staging.json
{
"baseUrl": "http://localhost:8080",
"defaultCommandTimeout": 5000,
"video": false,
"env": {
"staging": true
}
}
Then the continuous integration server would use the command npx cypress run --config-file staging.json
to use the later configuration file.
Note: make sure the top-level configuration values like baseUrl
, video
, and others are placed at the top-level of the configuration files and do not accidentally end up in the env
object. They won't work correctly inside the env
object!
Hmm, seems like most settings would be duplicated. We could still use the cypress.json
file and override the config values and the environment variables via command line arguments:
npx cypress run --config defaultCommandTimeout=5000 --env staging=true
I do not like the above approach, because it hides the intent and spreads the related settings across multiple files. In this blog post I will show how to implement extends
syntax in the Cypress JSON configuration file without waiting for the Cypress team to add support.
Plugins file
When Cypress loads your project, you can change any configuration or environment value programmatically from the plugins file. For example, in the bahmutov/config-extends-example repository we first print the current configuration from cypress/plugins/index.js
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
console.log(config)
return config
}
The list of settings is long ...
{
animationDistanceThreshold: 5,
fileServerFolder: '/Users/gleb/git/config-extends-example',
baseUrl: null,
fixturesFolder: '/Users/gleb/git/config-extends-example/cypress/fixtures',
blacklistHosts: null,
chromeWebSecurity: true,
modifyObstructiveCode: true,
...
experimentalGetCookiesSameSite: false,
experimentalSourceRewriting: false,
experimentalComponentTesting: false,
projectRoot: '/Users/gleb/git/config-extends-example',
configFile: '/Users/gleb/git/config-extends-example/cypress.json'
}
The very last setting configFile
is what we are after. It was added in Cypress v4.1.0 to allow the plugins file to "know" what configuration file has been loaded. Let's add a second configuration file extending the cypress.json
.
// cypress.json
{
"defaultCommandTimeout": 2000,
"video": true
}
// staging.json
{
"extends": "./cypress.json",
"video": false
}
Let's run Cypress using the staging.json
configuration file.
npx cypress run --config-file staging.json
...
long list of resolved config values
Notice the list of configuration values does NOT include the extends
property, because Cypress whitelists config keys. Here is where the plugins code can help. Because it "knows" which configuration file has been loaded, it can load it again as JSON, find the extends
property and load the base config file, then merge the two configs:
const deepmerge = require('deepmerge')
const path = require('path')
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
console.log(config)
const configJson = require(config.configFile)
if (configJson.extends) {
const baseConfigFilename = path.join(config.projectRoot, configJson.extends)
const baseConfig = require(baseConfigFilename)
console.log('merging %s with %s', baseConfigFilename, config.configFile)
return deepmerge(baseConfig, configJson)
}
return config
}
Notice that we return merged base config plus config JSON (not the config
argument passed into the plugins file). Cypress will automatically re-merge the resolved config with the returned result. Thus we only need to "worry" about our settings and not the full config.
We can see the final configuration values by running Cypress in the interactive mode
npx cypress open --config-file staging.json
Open the Settings / Configuration
tab and notice that the defaultCommandTimeout: 2000
comes from plugins
- this is the result of the merge returned from the cypress.json
base file. The value video: false
came from the config
which in our case was staging.json
configuration file.
We can load and merge configs recursively to allow severals extends
levels.
const deepmerge = require('deepmerge')
const path = require('path')
function loadConfig(filename) {
const configJson = require(filename)
if (configJson.extends) {
const baseConfigFilename = path.join(
path.dirname(filename), configJson.extends)
const baseConfig = loadConfig(baseConfigFilename)
console.log('merging %s with %s', baseConfigFilename, filename)
return deepmerge(baseConfig, configJson)
} else {
return configJson
}
}
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
return loadConfig(config.configFile)
}
I have placed the recursive config load into bahmutov/cypress-extends and published it as the @bahmutov/cypress-extends
NPM package. Give it a try:
npm i -D @bahmutov/cypress-extends
# or
yarn add -D @bahmutov/cypress-extends
Then use it from the plugins file
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
return require('@bahmutov/cypress-extends')(config.configFile)
}
Tip: add a schema URL to your Cypress configuration file to get code completion for config settings
{
"$schema": "https://on.cypress.io/cypress.schema.json"
}