Stub Objects By Passing Them Via Window Property

How to change the internal application objects during Cypress tests by assigning them to the window property.

Imagine you have an application that imports a config object from another file, and then uses the config to control what it shows on the page.

Config.tsx
1
2
3
4
export default {
title: 'Todo App',
showCount: true,
}
App.tsx
1
2
3
4
import Config from './Config'
function App() {
return (<h1>{Config.title}</h1>)
}

Can you change the Config object from a Cypress end-to-end test?

🎁 You can find the source code in the repo bahmutov/stub-window-object-example

Set Config on the window

To let the test "know" the config values, the simplest is to assign it to a property of the window object.

App.tsx
1
2
3
4
5
6
7
8
9
10
11
import Config from './Config'

// @ts-ignore
if (window.Cypress) {
// @ts-ignore
window.Config = Config
}

function App() {
return (<h1>{Config.title}</h1>)
}

The test can confirm the keys in the config object, and use the title from the config to check the page.

cypress/integration/config-spec.js
1
2
3
4
5
6
7
8
9
10
it('sets the window config', () => {
cy.visit('/')
// cy.visit yields the window object
.should('have.property', 'Config')
.should('have.keys', 'title', 'showCount')
.its('title')
.then((title) => {
cy.contains('h1', title)
})
})

Checking the title

Tip: I am using // @ts-ignore above some source lines to avoid fighting the TypeScript compiler.

Not today, TypeScript

Override the config object

If we can read the config object from the window object, we can also override it. We just need to define a custom property that lets us return a new value from the test. In the application, we make an intermediate variable to pass the config object.

App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
import _config from './Config'

let Config = _config

// @ts-ignore
if (window.Cypress) {
// @ts-ignore
window.Config = Config
// @ts-ignore
Config = window.Config
}
// use the Config object

Our Cypress test can intercept the assignment "window.Config = Config" and replace its value and return its own object. We just need to make sure we are ready before the application loads its application code. We can do this in the onBeforeLoad callback of the cy.visit command.

cypress/integration/override-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('overrides the config', () => {
cy.visit('/', {
onBeforeLoad(win) {
Object.defineProperty(win, 'Config', {
// ignore the config the application sets
// and instead use our own test config object
set: cy.stub().as('setConfig'),
get() {
return {
title: 'Cypress Test',
showCount: false,
}
},
})
},
})
cy.contains('h1', 'Cypress Test')
cy.get('[data-cy="pending-count"]').should('not.exist')
})

The test returns its own config object

Combine the config object

We do not have to completely replace the application's config object. We can combine its value with some of our test properties.

cypress/integration/combine-spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it('combines the config', () => {
cy.visit('/', {
onBeforeLoad(win) {
let appValue
Object.defineProperty(win, 'Config', {
set(v) {
appValue = v
},
get() {
// returns a combined config
return {
...appValue,
title: 'Cypress Test',
}
},
})
},
})
// we only check the property we have set
cy.contains('h1', 'Cypress Test')
})

The test returns its own config object

Mock Config module

We can go one step beyond the previous solutions. Instead of modifying the application code to expose the Config object, we can directly mock the src/Config.tsx module in our JavaScript bundle (assuming the "standard" Webpack module format). We can do this using the plugin mock-in-bundle

1
2
$ npm i -D mock-in-bundle
+ [email protected]

From the spec file, specify the module and the new default export.

cypress/integration/mock-spec.js
1
2
3
4
5
6
7
import { mockInBundle } from 'mock-in-bundle'

it('mocks the Config module', () => {
mockInBundle('src/Config.tsx', { default: { title: 'Mock Test' } })
cy.visit('/')
cy.contains('h1', 'Mock Test')
})

Boom 💣 the mock title is shown.

The Cypress test mocks the module by name in the JavaScript bundle loaded by the application

Happy testing!