cypress-data-session

Cypress command for flexible test data setup

Usage no npm install needed!

<script type="module">
  import cypressDataSession from 'https://cdn.skypack.dev/cypress-data-session';
</script>

README

cypress-data-session

ci status renovate-app badge cypress version

Cypress command for flexible test data setup

Read the blog post Flexible Cypress Data Setup And Validation and Faster User Object Creation

Videos

Install

$ npm install -D cypress-data-session
# or using Yarn
$ yarn add -D cypress-data-session

Import this package from the spec or from the support file

// cypress/support/index.js
import 'cypress-data-session'

If you plan to use the shareAcrossSpecs option, you need to load this plugin from your plugin file

// cypress/plugin/index.js
module.exports = (on, config) => {
  require('cypress-data-session/src/plugin')(on, config)
}

Types

If using JavaScript, point the spec at this package using the /// comment

// cypress/integration/spec.js
/// <reference types="cypress-data-session" />

See src/index.d.ts

Use

You can call cy.dataSession with simple 3 arguments or with an options object that allows you to pass more options.

three arguments

cy.dataSession(sessionName, setupFunction, validateValueFunction)

This example comes from cypress/integration/spec.js

beforeEach(() => {
  // let's say you want to set up the value "A"
  cy.dataSession(
    'A', // data name
    () => 'a', // data creation commands
    (x) => x === 'a', // data validation function
  )
})

it('has object A', () => {
  expect(Cypress.getDataSession('A')).to.equal('a')
})

The value is automatically added as an alias, so you can use function () { ... } syntax for the test callback and access the above value using this.A property

it('exists under an alias', function () {
  expect(this.A).to.equal('a')
})

Note: if the setup function's text changes, the session is recomputed.

validate

The validate predicate can use Cypress commands, but must ultimately yield a boolean value.

// validate a room id by fetching it from the database
// using cy.task and finding the room
const validate = (id) => {
  return cy.task('getRooms', null, { log: false }).then((rooms) => {
    // important: make sure to return a Boolean
    // otherwise _.find returns undefined
    // and Cypress cy.then callback returns the previous value
    return Boolean(Cypress._.find(rooms, { _id: id }))
  })
}

The validate function is optional. If you skip it, the setup will be run every time.

// without "validate" function, the "setup" runs every time
cy.dataSession(name, setup)

You can also pass a boolean value, which is useful during debugging.

validate: true // no need to recompute the value, it is still valid
validate: false // call setup again

See cypress/integration/validate.js spec.

onInvalidated

You can pass a function as the last argument to the cy.dataSession to be called if the "validate" returns false. This function will be called before the "setup" function executes.

function onInvalidated() {
  // clear user session for example
}
cy.dataSession(name, setupUser, validateUser, onInvalidated)

See an example in the spec cypress/integration/invalidate.js and video Invalidate cy.session From cypress-data-session.

Options object

You can pass multiple parameters using a single options object, see cypress/integration/options.js

cy.dataSession({
  name: 'C',
  setup: () => 'c',
  validate: (x) => x === 'c',
})

recreate

Using the options object you can pass another function to be called after the validate yields true. This function let's you to perform Cypress commands with the validated value to "finish" the recreation. For example, you could visit the page after setting the cookie from the data session to end on the page, just like the setup does.

cy.dataSession({
  name: 'logged in',
  setup: () => {
    // create user, visit the page, log in
    // let's say we are on the page '/home
    // save the cookie in the data session
    cy.getCookie('connect.sid')
  },
  // assume the cookie is valid
  validate: (c) => true,
  recreate (cookie) => {
    cy.setCookie(cookie)
    cy.visit('/home')
  }
})

shareAcrossSpecs

By default, the data session value is saved inside Cypress.env object. This object is reset whenever the spec gets reloaded (think Cmd+R press or the full browser reload). The object is gone when the cypress run finishes with a spec and opens another one. If you want the data value to persist across the browser reloads, or be shared across specs, use the shareAcrossSpecs: true option.

cy.dataSession({
  name: 'shared value',
  setup: () => 'a',
  validate: (x) => x === 'a',
  shareAcrossSpecs: true,
})

The first spec that creates it, saves it in the plugin file process. Then other specs can re-use this value (after validation, of course).

preSetup

Sometimes you might need to run a few steps before the setup commands. While you could have these commands inside the setup function, it might make clear to anyone reading the code that these commands are preparing the data for the setup function. For example, you could check if the user you are about to create exists already and needs to be deleted first.

cy.dataSession({
  name: 'user',
  preSetup() {
    cy.task('findUser', 'Joe').then((user) => {
      if (user) {
        cy.task('deleteUser', user._id)
      }
    })
  },
  setup() {
    // create the user "Joe"
  },
  validate(saved) {
    // check if the user "Joe" exists
  },
})

dependsOn

A data session can depend on another data session or even multiple data sessions. For example, the data session "logged in user" can depend on the "created user" data session. If the "parent" session changes, we need to recompute all sessions that depend on it.

cy.dataSession({
  name: 'created user',
  setup() {
    // create a user
  },
})

cy.dataSession({
  name: 'logged in user',
  dependsOn: 'created user',
  setup() {
    // take the user object from
    // the data session "created user"
    // and log in
  },
})

If the "created user" gets invalidated and recomputed (its setup method runs again), then the data session "logged in user" is invalidated automatically.

To list dependencies on multiple data sessions, pass an array of names

dependsOn: ['first', 'second', 'third']

init

Sometimes the data is generated, but sometimes we want to first see if we can load / initialize it the very first time (when there is nothing in the cache). This is where the init callback is useful. For example, let's say we are creating a special test user using the method setup and store its ID in the cache.

cy.dataSession({
  name: 'test user',
  setup() {
    cy.task('createUser') // yields the user ID
  },
  validate: Cypress._.isString,
})

But what happens if there is a test user already created by the previous tests? The very first time we run the data session, we want to check if there is a test user, and if it exists, store its ID and NOT call the setup function. Thus we add the init function

cy.dataSession({
  ...
  init () {
    cy.task('findUser', 'test user') // yields the ID if found
  }
})

The above example will find the test user (by calling the cy.task) the very first time the command runs, since there is nothing in the data session yet. Then it validates the ID. If there is no ID, it runs the setup function to create the test user. If there is a test user, and it returned the ID from the init method, then it is cached (as if the setup method created it), and the test continues.

Flow

Consider the cy.dataSession(options) where the options object might have the following method callbacks: validate, init, setup, preSetup, recreate, and onInvalidated. Your case might provide just some of these callback functions, but let's say you have provided all of them. Here is how they will be called.

  • First, the code pulls cached data for the session name.
  • if there is no cached value:
    • it calls the init method, which might return a value
    • if there is a value, and it passes validate callback
      • it saves the value in the data session and finishes
      • else it needs to generate the real value and save it
        • it calls preSetup and setup methods and saves the value
  • else (there is a cached value):
    • it calls validate with the cached value
    • if the validate returns true, the code calls recreate method
    • else it has to recompute the value, so it calls onInvalidated, preSetup, and setup methods

Flowchart

Flowchart

Flowchart source
flowchart TD
  A[Start] --> B{Have\ncached\nvalue?}
  B --> |No cached value| C[calls init]
  C --> |init result| D{calls\nvalidate}
  D --> |validated| J[recreate]
  J --> E[Save the value]
  E --> F[Finish]
  D --> |Not validated| H[onInvalidated]
  H --> G[preSetup & setup]
  G --> E
  B --> |With cached value| D

Examples

Global methods

A few global utility methods are added to the Cypress object for accessing the data sessions. These methods are mostly utilities used internally.

  • Cypress.getDataSession(name)
  • Cypress.getDataSessionDetails(name)
  • Cypress.getSharedDataSessionDetails(name)
  • Cypress.clearDataSession(name)
  • Cypress.clearDataSessions()
  • Cypress.dataSessions(enable)
  • Cypress.setDataSession(name, value)
  • Cypress.formDataSessionKey(name)
  • Cypress.printSharedDataSessions

getDataSession

Returns the data for the session with the given name (if any).

getDataSessionDetails

Returns the session with data and internal details, useful for debugging. If the session is not in memory, but stored in the plugin process, use getSharedDataSessionDetails.

getSharedDataSessionDetails

Fetches the session details from the plugin process.

dataSessions

Without any arguments, this static method lists all current data sessions.

clearDataSessions

Clears all current data sessions from memory.

clearDataSession

Clears the given data session from memory, from aliases, and from the plugin space (if shared). Should be used inside .then callback or in a hook, since it executes immediately. Can be used from the DevTools console

cy.dataSession('user', ...)
// rest of cy commands
cy.then(() => {
  // remove the data session "user"
  Cypress.clearDataSession('user')
})

Debugging

This plugin uses debug module to output verbose messages. Start Cypress with the environment variable DEBUG=cypress-data-session to see them. How to set an environment variable depends on the operating system. From a Linux terminal we can use

$ DEBUG=cypress-data-session npx cypress open

See also

Custom command creation and publishing to NPM described in these blog posts:

Small print

Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2021

License: MIT - do anything with the code, but don't blame me if it does not work.

Support: if you find any problems with this module, email / tweet / open issue on Github

MIT License

Copyright (c) 2021 Gleb Bahmutov <gleb.bahmutov@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.