In this guide, we'll explore how to perform snapshot testing for HTML files using Node.js. This approach is particularly useful for catching regressions, allowing for manual inspection of differences, and updating snapshots as needed.
We'll focus on snapshot testing for HTML files generated for inclusion in email reports. Since we're dealing with single-file HTML documents, the solution is straightforward and builds on previous snapshot testing techniques.
To simplify our code, we'll use a few Node.js modules. You can install these modules using:
yarn add lodash diff colors
First, we'll define the paths for storing our expected and actual HTML files.
These files are stored in a sub-directory (__html-snapshots
) to
keep them organized and easy to locate.
const reportPath = path.join(__dirname, '__html-snapshots'); const actualFileName = path.join(reportPath, 'actual.html'); const expectedFileName = path.join(reportPath, 'expected.html');
In our scenario, the HTML file is generated in memory and stored in a database table for email queuing. For testing, we need a utility function to write the actual contents to a file for comparison.
const writeActualHtmlFile = async (data, outputFilePath) => { const outputDirectoryPath = path.dirname(outputFilePath); await mkdirIfNotExists(outputDirectoryPath); return new Promise((resolve, reject) => { fs.writeFile(outputFilePath, data, (err) => { if (err) { reject(err); } else { resolve(); } }); }); };
Next, we'll define a function to compare the newly generated (actual) HTML
file against the expected HTML file. The comparison is done in a
compareFiles
function. If the files don't match, we'll output the
differences in a color-coded format.
export const isHtmlEqual = (aFilePath, bFilePath) => { const aFileContents = fs.readFileSync(aFilePath).toString(); const bFileContents = fs.readFileSync(bFilePath).toString(); const areFilesEqual = compareFiles(aFileContents, bFileContents); if (!areFilesEqual) { console.info('-------------------------------------------------------------------------------'); console.info(`${aFilePath} <==> ${bFilePath}:`); console.info('-------------------------------------------------------------------------------'); const results = diff.diffLines(aFileContents, bFileContents); for (const result of results) { const formattedLine = colorFormatForFileDiff(result); if (formattedLine) { console.info(trimEnd(formattedLine)); } } console.info('-------------------------------------------------------------------------------'); console.info(); } return areFilesEqual; };
Let's set up our comparison:
import * as _ from 'lodash'; import * as diff from 'diff'; const colors = require('colors'); // set up some reasonable colors colors.setTheme({ filePath: 'grey', unchanged: 'grey', added: 'red', removed: 'green', });
We'll define a simple function to compare the actual and expected files as strings. This approach works well for files that aren't too large.
const compareFiles = (aFileContents, bFileContents) => { return aFileContents === bFileContents; };
Finally, we'll define functions to format the differences in a human-readable way.
const colorFormat = (result) => { const value = result.value; if (result.added) { return colors.added(formattedLine(value, '-')); } if (result.removed) { return colors.removed(formattedLine(value, '+')); } return colors.unchanged(formattedLine(value, ' ')); }; const formattedLine = (line, prefix) => addLinePrefix(line, prefix); const addLinePrefix = (line, prefix) => { return trimEnd(line) .split(/(\r\n|\n|\r)/) .filter((segment) => segment.trim().length > 0) .map((segment) => `${prefix} ${trimEnd(segment)}`) .join('\n'); }; const trimEnd = (text) => { return text.replace(/[\s\uFEFF\xA0]+$/g, ''); };
Now, we'll define a snapshot function similar to our Excel and PDF testing:
If the expected and actual HTML don't match, we can manually inspect them. If
satisfied, rerun the test with the UPDATE
environment variable
set to overwrite the expected file with the actual file.
export const snapshot = async (actualFilePath, expectedFilePath) => { if (process.env.UPDATE || !(await exists(expectedFilePath))) { await copyFile(actualFilePath, expectedFilePath); } else { const helpText = [ '', '-------------------------------------------------------', `Actual contents of HTML file did not match expected contents.`, `Expected: ${expectedFilePath}`, `Actual: ${actualFilePath}`, '-------------------------------------------------------', '', ].join('\n'); const isDocumentEqual = await isHtmlEqual(expectedFilePath, actualFilePath); if (!isDocumentEqual) { console.error(helpText); } return expect(isDocumentEqual, 'HTML documents are not equal').to.be.true; } };
Finally, we'll write a simple test to exercise this method.
describe('HTML Files', () => { it('can generate an HTML file', async () => { // generate test data generateTestData(); // fetch actual daily email report const actualHtml = await getDailyEmailReport(); // write the email to our expected path. await writeActualHtmlFile(actualHtml, actualFilePath); // compare snapshot of actual and expected HTML files. await snapshot(); }); });
First, we'll run the test.
We can see that both the actual and expected HTML files have the same timestamp.
Next, we'll run our test again to see that only the actual HTML file has been updated.
We can see that the timestamp for the actual HTML file has changed, but the expected HTML file hasn't.
Then, we'll modify our implementation and re-run our test.
We can see that our test detected a change between the actual and expected HTML files and reported it as a test failure. At this point, we will manually inspect the expected HTML file and actual HTML file to visually compare the two. If, after manually inspecting the expected and actual HTML file, we find that these changes are acceptable, we can simply re-run our test with the UPDATE environment variable set.
Finally, we can see that the timestamp of the expected HTML file is updated.
We can add this new expected HTML file to our repo and commit. If we are using a continuous integration environment, we will automatically see a test failure when the actual output differs from the expected output. And that's all there is to it!