This project contains a couple of utilities for writing and testing HTML in JavaScript/Node.js. They can also be ported to other languages relatively easily.
These utilities are not available on npm, but they are meant to be copy-pasted into your project. They come complete with unit tests, so they can be evolved to fit your project's needs.
There is a html
tag function for writing tagged templates, to produce HTML without a heavy framework like React.
The placeholders are automatically escaped to avoid XSS vulnerabilities.
const input = "<script>alert(1)</script>"
const output = html`<p>Hello ${input}</p>`
expect(output.html).toBe("<p>Hello <script>alert(1)</script></p>")
You can create components using plain old functions. Works great with htmx.
function homePage() {
return html`
<h1>Welcome</h1>
<p>${clickerButton()}</p>
`
}
function clickerButton(counter: number = 0) {
return html`
<button hx-get="/clicker?counter=${counter + 1}"
hx-target="this"
hx-swap="outerHTML">
Clicked ${counter} times
</button>
`
}
If you need to only parameterize the value of an attribute, the html
template is enough on its own.
But if you need to also toggle attributes at runtime, there is an optional attrs
helper function.
const name = "fruits"
const value = "banana"
const currentValue = "banana"
expect(html`
<input type="radio" ${attrs({name, value, checked: currentValue === value})}>
`.html).toBe(`<input type="radio" name="fruits" value="banana" checked>`)
It has some convenience features for toggling CSS classes and writing inline styles.
const toggle = false
expect(html`
<p ${attrs({
class: ["foo", "bar", toggle && "gazonk"],
style: {
border: "1px solid blue",
"background-color": "red",
},
})}></p>
`.html).toBe(`<p class="foo bar" style="border: 1px solid blue; background-color: red"></p>`)
Read the tests to learn more what this templating library can do.
See html-templates.ts and html-templates.test.ts
There is a visualizeHtml
function for extracting the visible text from HTML, to easily unit test HTML templates.
The components mentioned in the previous section could be unit tested like this:
expect(visualizeHtml(homePage())).toBe(normalizeWhitespace(`
Welcome
Clicked 0 times`))
const button = clickerButton(5)
expect(visualizeHtml(button)).toBe("Clicked 5 times")
expect(button.html).toContain(`hx-get="/clicker?counter=6"`)
By default visualizeHtml
strips all HTML tags.
But if you add a data-test-icon
attribute, its value will be shown in front of the element.
This enables stringly asserted
testing to assert non-textual information.
expect(visualizeHtml("<p>one</p><p>two</p>")).toBe("one two")
expect(visualizeHtml(`<label><input type="checkbox" checked data-test-icon="☑️"> Toggle</label>`))
.toBe("☑️ Toggle")
In addition to DOM elements, HTML strings and our HTML templates, visualizeHtml
works also for
React elements.
If you use only one templating library, you can delete a few lines from visualizeHtml
to remove the other dependency.
See html-testing.ts and html-testing.test.ts
The previous visualizeHtml
is implemented using just regular expressions.
Regular expressions are fast and portable, but they have limited expressiveness.
In html-testing2.ts there is a more flexible parser-based implementation of visualizeHtml
.
It adds support for a data-test-content
attribute that will replace the element's content with a custom visualization.
(The data-test-icon
only adds text in front of the element.)
This makes it capable of visualizing e.g. <textarea>
and <select>
elements.
expect(visualizeHtml2(`<textarea data-test-content="[foo]">foo</textarea>`)).toBe("[foo]")
expect(visualizeHtml2(`
<select name="pets" data-test-content="[Cats]">
<option value=""></option>
<option value="cats" selected>Cats</option>
<option value="dogs">Dogs</option>
</select>
`)).toBe("[Cats]")
This is the most extensible implementation.
The parser-based approach makes it easy to e.g. implement default visualizations for form elements (left as an exercise
for the reader), so that you don't need to sprinkle data-test-content
attributes as much.
See html-testing2.ts and html-testing2.test.ts
If the tests are run using a web browser instead of Node.js, we can also implement visualizeHtml
by relying on
HTMLElement.innerText
which is aware of the rendered appearance of the text content.
Using a browser will enable CSS, so that you can hide elements with display: none
.
Otherwise, this implementation works the same as the previous advanced implementation.
See html-testing3.ts and html-testing3.test.ts
To use this library, download it to be part of your project. In particular, the testing library often benefits from project-specific customizations.
This library is finished software, so there is no need to get constant updates to it. No new features is a feature in itself. You wouldn't want to be left-padded because of just 50 lines of code, would you?
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-templates.ts
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-templates.test.ts
Regex-based implementation. Can be easily ported to any language.
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing.ts
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing.test.ts
Parser-based implementation. Requires a web browser, jsdom or similar. Porting to other languages requires an HTML parser.
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing2.ts
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing2.test.ts
Requires a web browser.
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing3.ts
wget https://raw.githubusercontent.com/luontola/html-utils/refs/heads/main/src/html-testing3.test.ts