Compute SHA256 from HTML
Compute and compare SHA codes
📺 Watch this recipe explained in the video Compute And Compare SHA-256 From HTML.
<div id="greeting">
<span id="form">Hello</span>, <span id="target">World</span>!
</div>
Here is an example async function computing the SHA-256 digest using the browser built-in APIs. Take from Mozilla Docs
async function digestMessage(message) {
// encode as (utf-8) Uint8Array
const msgUint8 = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest(
'SHA-256',
msgUint8,
) // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('') // convert bytes to hex string
return hashHex
}
We can compute SHA of any element (I like trimming the HTML string before computing its digest)
cy.get('#greeting')
.invoke('html')
.invoke('trim')
.then(digestMessage)
.should('be.a', 'string')
Let's confirm the SHA changes when the HTML inside the element changes.
cy.get('#greeting')
.invoke('html')
.invoke('trim')
.then(digestMessage)
.should('be.a', 'string')
.then((sha) => {
// change something on the page
cy.get('#target').invoke('text', 'You')
cy.get('#greeting')
.invoke('html')
.invoke('trim')
// sha changes
.then(digestMessage)
.should('not.equal', sha)
})
If we plan to compute SHA across our project, we can create a custom child command
Cypress.Commands.add(
'sha',
{ prevSubject: 'element' },
($el) => {
// put a message to Command Log
Cypress.log({ name: 'sha' })
const html = $el.html().trim()
return digestMessage(html)
},
)
Let's use the new cy.sha
command to confirm the SHA changes when the element's HTML changes
cy.get('#greeting')
.sha()
.then((t) => {
cy.get('#target').invoke('text', 'everyone')
cy.get('#greeting').sha().should('not.equal', t)
})
Warning: the above test is bad practice. The SHA of HTML can change for many reasons, your tests should confirm the HTML changes in the expected way.
Avoid using SHA only
Using SHA hash code to confirm the element changes is a bad practice in opinion. It literally says "element's HTML should not be THAT", while a good practice would use a positive assertion "element should be X". There are lots of reasons the element's HTML changes - and those changes might not matter to the user at all. Here is a situation that passes the "SHA changes" test, but shows a broken page.
<button id="load">Load data</button>
<div id="output" />
<script>
document
.getElementById('load')
.addEventListener('click', () => {
document.getElementById('output').innerHTML = `
<div>Error loading...</div>
`
})
</script>
Let's click on the button and confirm the output changes its content.
async function digestMessage(message) {
// encode as (utf-8) Uint8Array
const msgUint8 = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest(
'SHA-256',
msgUint8,
) // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('') // convert bytes to hex string
return hashHex
}
Cypress.Commands.add(
'sha',
{ prevSubject: 'element' },
($el) => {
// put a message to Command Log
Cypress.log({ name: 'sha' })
const html = $el.html().trim()
return digestMessage(html)
},
)
cy.get('#output')
.sha()
.then((sha) => {
cy.contains('button', 'Load data').click()
cy.get('#output').sha().should('not.equal', sha)
})
The page does change and the element's HTML has a new SHA, but is this a good test? Absolutely not. The page shows an error element, not successfully loaded data. The test should confirm one successful output, not try to assertion that a million different possible other outputs do not appear.
SHA code changes even when the only changes are invisible to the user, like whitespace around elements.
cy.log('a space at the end')
cy.wrap(
Promise.all([
digestMessage('<div>Hello</div>'),
digestMessage('<div>Hello</div> '),
]),
{ log: false },
).then(([sha1, sha2]) => {
expect(sha1, 'extra space at the end').to.not.equal(sha2)
})
Note: there is also a subtle timing bug in the above test, something that affects every test framework that relies only on promises or async/await
syntax to execute its commands.
A better test
A much better test would confirm the specific expected output appears.
<button id="load">Load data</button>
<div id="output" />
<script>
document
.getElementById('load')
.addEventListener('click', () => {
const error = '<div class="error">Error loading...</div>'
const data = '<div class="data">Joe 02177</div>'
// change the limit to simulate different outcomes
// if the limit is 0 then no errors will be shown ever
// if the limit is 1 only the error is shown
const showError = Math.random() < 0
document.getElementById('output').innerHTML = showError
? error
: data
})
</script>
cy.contains('button', 'Load data').click()
Use a positive assertion of what should appear during successful flow
cy.get('#output .data')
.should('be.visible')
// if you know something about the data, check it
.and('include.text', 'Joe')
After the positive assertion passes, confirm negative outcomes, like the error element should not exist
cy.get('.error').should('not.exist')
See also
- 📝 read the blog post Be Careful With Negative Assertions
- 📝 read the blog post Do Not Use SHA To Compare HTML During E2E Tests