How to extend cypress.json config to other configuration files?

3k Views Asked by At

In my Cypress 10 project, I have the following config files:

  • cypress/cypress.config.js

  • cypress/config/qa.json (Points at my QA environment).

  • cypress/config/staging.json (Points at my Staging environment).

Everything in the below cypress.config.js file is common across both QA & Staging environments:

const { defineConfig } = require("cypress");
const fs = require('fs-extra')
const readDirRecursive = require('fs-readdir-recursive')
const createBundler = require("@bahmutov/cypress-esbuild-preprocessor")
const preprocessor = require("@badeball/cypress-cucumber-preprocessor")
const createEsbuildPlugin = require("@badeball/cypress-cucumber-preprocessor/esbuild")
const stdLibBrowser = require('node-stdlib-browser')
const plugin = require('node-stdlib-browser/helpers/esbuild/plugin')
const mysql = require('mysql')

function queryTestDb(query, config) {
    // creates a new mysql connection using credentials from cypress.json env's
    const connection = mysql.createConnection(config.env.db)
        // start connection to db
    connection.connect()
        // exec query + disconnect to db as a Promise
    return new Promise((resolve, reject) => {
        connection.query(query, (error, results) => {
            if (error) reject(error)
            else {
                connection.end()
                return resolve(results)
            }
        })
    })
}

async function setupNodeEvents(on, config) {

    await preprocessor.addCucumberPreprocessorPlugin(on, config, {
        omitBeforeRunHandler: true
    })

    on('before:run', () => {
        fs.emptyDirSync('./test-results')
        preprocessor.beforeRunHandler(config)
    })

    on(
        'file:preprocessor',
        createBundler({
            inject: [require.resolve('node-stdlib-browser/helpers/esbuild/shim')],
            define: {
                global: 'global',
                process: 'process',
                Buffer: 'Buffer'
            },
            plugins: [plugin(stdLibBrowser), createEsbuildPlugin.default(config)],
        })
    )

    on('task', {
        readFolder(path) {
            return readDirRecursive(path)
        }
    })

    on('task', {
        queryDb: query => {
            return queryTestDb(query, config)
        }
    })

    return config
}

module.exports = defineConfig({
    defaultCommandTimeout: 30000,
    requestTimeout: 30000,
    responseTimeout: 60000,
    pageLoadTimeout: 90000,
    numTestsKeptInMemory: 1,
    chromeWebSecurity: false,
    experimentalWebKitSupport: false,
    screenshotsFolder: 'test-results/screenshots',
    videosFolder: 'test-results/videos',
    viewportWidth: 1920,
    viewportHeight: 1200,
    watchForFileChanges: false,
    screenshotOnRunFailure: true,
    video: false,
    videoCompression: 8,
    reporter: 'spec',
    reporterOptions: {
        mochaFile: 'test-results/tests-output/result-[hash].xml',
        toConsole: true
    },
    retries: {
        runMode: 1,
        openMode: 0
    },
    e2e: {
        setupNodeEvents,
        specPattern: 'cypress/tests/**/*.feature',
    },
})

Even though a lot of the values will remain the same for QA & Staging, I need to use different config files for them.

Specifically, the e2e.baseUrl & env.db values will be different in these environments. Every other value will be the same in QA & Staging.

Here is what I have tried to do in my qa.json file:

const { defineConfig } = require("cypress");

const baseConfig = require('../../cypress.config.js')

const fs = require('fs-extra')
const readDirRecursive = require('fs-readdir-recursive')
const createBundler = require("@bahmutov/cypress-esbuild-preprocessor")
const preprocessor = require("@badeball/cypress-cucumber-preprocessor")
const createEsbuildPlugin = require("@badeball/cypress-cucumber-preprocessor/esbuild")
const stdLibBrowser = require('node-stdlib-browser')
const plugin = require('node-stdlib-browser/helpers/esbuild/plugin')
const mysql = require('mysql')

const baseUrl = 'https://qa.com'

const env = {
    db: {
        host: 'myHost',
        user: 'myUser',
        password: 'myPassowrd',
        database: 'myDb',
    }
}

function queryTestDb(query, config) {
    // creates a new mysql connection using credentials from cypress.json env's
    const connection = mysql.createConnection(config.env.db)
        // start connection to db
    connection.connect()
        // exec query + disconnect to db as a Promise
    return new Promise((resolve, reject) => {
        connection.query(query, (error, results) => {
            if (error) reject(error)
            else {
                connection.end()
                return resolve(results)
            }
        })
    })
}

async function setupNodeEvents(on, config) {

    await preprocessor.addCucumberPreprocessorPlugin(on, config, {
        omitBeforeRunHandler: true
    })

    on('before:run', () => {
        fs.emptyDirSync('./test-results')
        preprocessor.beforeRunHandler(config)
    })

    on(
        'file:preprocessor',
        createBundler({
            inject: [require.resolve('node-stdlib-browser/helpers/esbuild/shim')],
            define: {
                global: 'global',
                process: 'process',
                Buffer: 'Buffer'
            },
            plugins: [plugin(stdLibBrowser), createEsbuildPlugin.default(config)],
        })
    )

    on('task', {
        readFolder(path) {
            return readDirRecursive(path)
        }
    })

    on('task', {
        queryDb: query => {
            return queryTestDb(query, config)
        }
    })

    return config
}

module.exports = defineConfig({
    defaultCommandTimeout: 30000,
    requestTimeout: 30000,
    responseTimeout: 60000,
    pageLoadTimeout: 90000,
    numTestsKeptInMemory: 1,
    chromeWebSecurity: false,
    experimentalWebKitSupport: false,
    screenshotsFolder: 'test-results/screenshots',
    videosFolder: 'test-results/videos',
    viewportWidth: 1920,
    viewportHeight: 1200,
    watchForFileChanges: false,
    screenshotOnRunFailure: true,
    video: false,
    videoCompression: 8,
    reporter: 'spec',
    reporterOptions: {
        mochaFile: 'test-results/tests-output/result-[hash].xml',
        toConsole: true
    },
    retries: {
        runMode: 1,
        openMode: 0
    },
    e2e: {
        setupNodeEvents,
        baseUrl: baseUrl,
        specPattern: 'cypress/tests/**/*.feature',
    },
    env: {
        ...baseConfig,     // values from imported file
        ...env,            // add or overwrite with values from above
        
    }
})

As you can see, I've managed to move my baseUrl & env values into qa.json.

However, there are still a lot of duplicate values in cypress.config.js & qa.json.

The command I am using to run the tests is npx cypress open --config-file cypress/config/aqua.js

How can I re-use the queryTestDb() function setupNodeEvents() function & the other config values in cypress.config.js?

3

There are 3 best solutions below

0
On

If you're willing to use lodash (looks like it's a dependency of cypress anyway), this worked for me in cypress 13

cypress.custom.js

const { defineConfig } = require("cypress");
const baseConfig = require("./cypress.config.js");
const { merge } = require("lodash");

module.exports = defineConfig(merge(baseConfig, {
    e2e: {
        baseUrl: "http://different-base-url",
        ...any other overrides
    },
}));

Then run npx cypress open --config-file ./cypress.custom.js

I wasn't sure what defineConfig was returning in my base cypress.config.js but it looks like you can use it as a normal object.

I used lodash's merge instead of Object.assign or spread because I wanted to deep merge the config.

0
On

While Fody's answer is really good - and led me in the right direction. Then I couldn't get it to work completely.

It also shows how to import a .json-file as baseConfig, where I wanted to have a .js-file, so I could have comments and more possibilities.

I solved this like this:

defaultConfig.js

module.exports = {

  paths: {
    cartPathName: 'cart',
    shopPathName: 'shop',
  },

  files: {
    tmpFolder: '/tmp',
    secretFolder: '/secret',
  },

  timeouts: {
    tinyTimeout: 3000,
    shortTimeout: 10000,
    mediumTimeout: 20000,
    longTimeout: 30000,
    hugeTimeout: 60000,
  },

  specPattern: [
    'cypress/e2e/some-custom-folder/some-test.cy.js',
    'cypress/e2e/**/*.js',
    'cypress/e2e/*.js',
  ],

  viewportWidth: 1250,
  viewportHeight: 1000
};

cypress.config.js

const { defineConfig } = require('cypress');
const defaultConfig = require('./defaultConfig');

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      return require('./cypress/plugins/index.js')(on, config);
    },
    video: false,
    specPattern: [
      ...defaultConfigs.specPattern
    ],
    ...defaultConfigs.viewportWidth,
    ...defaultConfigs.viewportHeight
  },
  env: {

    // Timeouts
    ...defaultConfigs.timeouts,

    // Files
    ...defaultConfigs.files,

    // URLs and paths
    ...defaultConfigs.paths,

    // Meta
    name: 'development',
    currentDomain: 'example.com',
  }
});
10
On

There's a package that does pretty much what you want to do.

cypress-extends

Cypress plugin that adds "extends" support to the configuration file. This allows the config files to remain DRY and avoid duplicating data, but still have multiple files for different scenarios.

// cypress.json
{
  "extends": "./base.json"
}
// base.json
{
  "fixturesFolder": false,
  "supportFile": false,
  "$schema": "https://on.cypress.io/cypress.schema.json"
}

In the docs Extending the Cypress Config File


Cypress v10

With the latest Cypress version, config changes from .json to .js file.

Now you can just import and merge the base.json, but you need to be careful about sub-sections of the comfig

cypress.config.js

const { defineConfig } = require('cypress')
const baseConfig = require('base.json')

module.exports = defineConfig({
  e2e: {
    baseUrl: baseConfig.baseUrl || 'http://localhost:1234'  // example OR 
  }
})

Or maybe this is more general

const { defineConfig } = require('cypress')
const baseConfig = require('base.json')

const e2e = {
  baseUrl: 'http://localhost:1234',
  fixturesFolder: false,
  supportFile: false,
  "$schema": "https://on.cypress.io/cypress.schema.json"
}

module.exports = defineConfig({
  e2e: {
    ...e2e,           // values from above
    ...baseConfig     // add or overwrite from imported file
  }
})

It's more powerful merging in javascript, but also easier to get wrong. Please refer to the Testing Type-Specific Options for a reference to what goes where in the new format.