Spy On DOM Methods And Properties

How to detect from the Cypress tests when the application is calling DOM methods and even sets innerHTML.

In the bahmutov/sorted-table-example example, the application sets and removes DOM element attributes:

app.js
1
2
3
4
function enableButtons() {
document.getElementById('sort-by-date').removeAttribute('disabled')
document.getElementById('sort-reverse').removeAttribute('disabled')
}

The application updates the table body by assigning the .innerHTML property

1
2
3
4
function sortTable() {
document.getElementById('people-data').innerHTML = listToHtml(sorted)
enableButtons()
}

Can we a) detect when the application is calling the .removeAttribute('disabled') method on a specific DOM element and b) spy on the .innerHTML property assignment?

Spy on removeAttribute

From our spec, first we need to access the DOM element, which we can do using $button[0] index. Then we use the standard cy.spy to create a spy around the button's removeAttribute method.

cypress/integration/spy-on-removeAttribute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
beforeEach(() => {
cy.visit('app/table.html')
})

it('spies on removeAttribute method', () => {
cy.get('#sort-by-date')
.then(($button) => {
// from jQuery object we need the actual DOM element
const button = $button[0]
cy.spy(button, 'removeAttribute')
.withArgs('disabled')
.as('removeAttribute')
})
.click()
cy.get('@removeAttribute').should('be.called')
})

It is almost too easy.

Spying on removeAttribute call on the given button

Spy on innerHTML property assignment

Spying / stubbing methods is easy, what about spying on the element's innerHTML = ... assignment? It is a little trickier, as we need to find the actual native DOM implementation via Object.getOwnPropertyDescriptor to call it from the spy. Browser DOM elements have a hierarchy of classes, and we need to travel up the prototype chain to find the implementation we can call ourselves. Here is my finding the property innerHTML using the $0 table body element as a start.

Getting the innerHTML property by going up the prototype chain

Luckily we can abstract finding any property descriptor into a utility function findPropertyDescriptor and then we put our test innerHTML descriptor with setter and getter onto the DOM element. Our implementation calls a sinon stub we construct with cy.stub command.

cypress/integration/spy-on-innerHTML.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
beforeEach(() => {
cy.visit('app/table.html')
})

function findPropertyDescriptor(obj, propName) {
if (!obj) {
return
}
return (
Object.getOwnPropertyDescriptor(obj, propName) ||
findPropertyDescriptor(Object.getPrototypeOf(obj), propName)
)
}

it('spies on innerHTML property', () => {
// wait for the initial table to be there
cy.get('tbody tr').should('have.length', 4)
cy.get('tbody').then(($tbody) => {
// prepare a stub that will be called when the application
// calls table.innerHTML = ... with sorted html
const setTable = cy.stub().as('setTable')

// use our own "el.innerHTML" to call the spy
// AND call the original "innerHTML"
const el = $tbody[0]
const ownProperty = findPropertyDescriptor(el, 'innerHTML')
expect(ownProperty, 'innerHTML descriptor').to.not.be.undefined

// direct all "set" and "get" calls to the native implementation
Object.defineProperty(el, 'innerHTML', {
get() {
return ownProperty.get.call(el)
},
set(newHtml) {
// plus call our test stub
setTable()
ownProperty.set.call(el, newHtml)
},
})
})

cy.contains('button', 'Sort by date').click()
// once our spy is called, that means the application
// has called "tbody.innerHTML = ..." and the table is sorted
cy.get('@setTable').should('be.called')
})

Spying on the application setting the table innerHTML property

Almost too easy.

📚 For more stub and spies examples, see my Spies, Stubs & Clocks page and this Cypress Guide.