Development

How TDD Shaped HTML6

Austin Brage
Sep 18, 2026
4 min read
TDD Testing HTML6
How TDD Shaped HTML6

Test-first, not "add tests later"

Before HTML6, my testing habit was the common one: build the feature, then backfill a handful of tests. Confirm the obvious cases work, move on.

My manager at Eldøy pushed something stricter, a practice called Test-Driven Development (TDD): write a focused unit test for a function before writing the function. Make sure it actually covers the range of inputs that function could see, not just the happy path.

repeat, one test case at a time Write a failing test red Make it pass green Refactor still green
The cycle behind every function in HTML6: a failing test first, the smallest change that passes it, then cleanup, before moving to the next case.

It felt slower at first: writing the test before the function means deciding what it should do before getting pulled into how it'll do it. It stopped feeling like overhead once that habit meant every new function started from a clear, narrow spec instead of a blank page.

Building one function at a time

HTML6 compiles a template through a pipeline of small, separate pieces:

  • Parser — turns raw HTML into a tree
  • Transpiler — turns that tree into a render function
  • Masking — hides special tags (if, elsif, else, map) behind placeholders during compilation
  • Chain-walker — resolves if/elsif/else sequences across sibling elements
  • Topological sort — orders component compilation by dependency

Each one lives in its own file. Each one got its own test suite before it was wired into the next piece.

Code with a real test suite around it is code you can change with confidence. Code you're only testing indirectly, through the whole pipeline, is code you're afraid to touch.

A bug caught before it ever shipped

This is where test-first actually earns its name. The test isn't written after you find a bug. Writing it is how you find the bug.

Take a simple template: loop over a list and render each item.

<li map="p of ps">{{p}}</li>

Covering the range of inputs, the way the habit demands, means testing what happens when the list itself is null, not just when it's empty. Writing that test is what surfaced the problem: the compiled code called .map() directly on whatever came in, and .map() on null throws.

Red first:

return mapArg.map(function(project) { ... })

Then the fix, one line:

return (mapArg || []).map(function(project) { ... })

Green. And the test that found the bug is the same one that now guards against it coming back:

test('map - empty', async ({ t }) => {
  var page = '<ul><li map="p of ps">{{p}}</li></ul>'
  var renderer = html.compile(page)
  var data = { ps: null }
  var result = renderer.render(data)
  t.equal(result, '<ul></ul>')
})

That's the actual habit test-first builds: not "never break anything," but "find the break yourself, before anyone else does, by testing the case you'd otherwise have skipped."

What it changed

This discipline is a big part of why I could later rewrite HTML6's entire compiler for speed with real confidence, instead of it being a leap of faith. Rewriting code that's genuinely covered, function by function, is a different kind of risk than rewriting code held together by manual spot-checks and hope.

Enjoyed this article?

Follow me for more content on web development, design, and technology.