Browser-based HTML test runner with TAP output for testing Declarative Shadow DOM and enhance-dsd generated HTML.
- ✅ Zero external dependencies - Uses only Node.js built-ins
- ✅ TAP v13 output - Compatible with standard TAP consumers
- ✅ Browser & CI support - Works in both interactive and headless modes
- ✅ Native DOM testing - Test real Declarative Shadow DOM in the browser
- ✅ Import maps - Use bare specifiers for framework and utilities
- ✅ Self-contained tests - HTML and assertions in the same file
- ✅ Simple HTTP server - Built-in server for local and CI testing
npm install @enhance/html-testCreate a file named test/my-component.test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test: My Component</title>
</head>
<body>
<!-- Your HTML to test -->
<my-component enhanced="✨">
<template shadowrootmode="open">
<style>:host { display: block; }</style>
<slot></slot>
</template>
Hello World
</my-component>
<!-- Tests -->
<script type="module">
import { test, assert } from '@enhance/test-framework';
test('has shadow root', () => {
const el = document.querySelector('my-component');
assert.ok(el.shadowRoot, 'Should have shadow root');
});
test('has enhanced attribute', () => {
const el = document.querySelector('my-component');
assert.strictEqual(el.getAttribute('enhanced'), '✨');
});
</script>
</body>
</html>Interactive mode (opens in browser):
npm start
# or
html-testHeadless mode (for CI):
html-test --headlesshtml-test [options]
Options:
--port, -p <port> Port to run server on (default: 3000)
--root, -r <path> Root directory to serve files from (default: cwd)
--headless, -h Run in headless Chrome mode
--timeout, -t <ms> Test timeout in milliseconds (default: 60000)# Start interactive server on port 8080
html-test --port 8080
# Run headless tests with custom root
html-test --headless --root ./test
# CI/CD mode with TAP output
html-test --headless > test-results.tapTest files are standard HTML files with .test.html extension containing:
- HTML to test - Your component markup, including Declarative Shadow DOM
- Test script - ES module that imports the test framework and defines tests
<!DOCTYPE html>
<html>
<head>
<title>Test: Component Name</title>
</head>
<body>
<!-- HTML being tested -->
<my-element>
<template shadowrootmode="open">
<p>Shadow content</p>
</template>
</my-element>
<!-- Test script -->
<script type="module">
import { test, assert, describe } from '@enhance/test-framework';
describe('my-element', () => {
test('renders correctly', () => {
const el = document.querySelector('my-element');
assert.ok(el);
});
});
</script>
</body>
</html>Define a test.
test('component exists', () => {
const el = document.querySelector('my-component');
assert.ok(el);
});Group related tests (optional).
describe('my-component', () => {
test('has shadow root', () => {
assert.shadowRoot('my-component');
});
test('renders content', () => {
assert.textContent('my-component', 'Expected text');
});
});assert.ok(value, message)- Value is truthyassert.not(value, message)- Value is falsyassert.strictEqual(actual, expected, message)- Strict equality (===)assert.equal(actual, expected, message)- Loose equality (==)assert.deepEqual(actual, expected, message)- Deep object/array equalityassert.match(string, regex, message)- String matches regexassert.throws(fn, message)- Function throws an error
assert.querySelector(selector, message)- Element exists (returns element)assert.textContent(selector, expected, message)- Element text content matchesassert.attribute(selector, attr, expected, message)- Attribute value matchesassert.hasAttribute(selector, attr, message)- Element has attributeassert.shadowRoot(selector, message)- Element has shadow root (returns shadowRoot)
The test runner automatically provides an import map with:
@enhance/test-framework- The test frameworktest-utils- Your localtest/utils.jsfile (if it exists)- All packages in the monorepo
Create test/utils.js:
export function shadowQuery(selector, shadowSelector) {
const host = document.querySelector(selector);
return host?.shadowRoot?.querySelector(shadowSelector);
}
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Use in tests:
import { test, assert } from '@enhance/test-framework';
import { shadowQuery, sleep } from 'test-utils';
test('finds element in shadow DOM', () => {
const el = shadowQuery('my-component', 'p');
assert.ok(el);
});Tests output TAP version 13 format:
TAP version 13
# Subtest: has shadow root
ok 1 - has shadow root
---
duration_ms: 2.50
type: 'test'
...
# Subtest: has enhanced attribute
ok 2 - has enhanced attribute
---
duration_ms: 1.25
type: 'test'
...
1..2
# tests 2
# pass 2
# fail 0
# duration_ms 3.75
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- name: Install Chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
sudo apt-get update
sudo apt-get install google-chrome-stable
- name: Run HTML tests
run: npx html-test --headlessimport { createTestServer, collectTAP } from '@enhance/html-test';
// Create and start server
const server = createTestServer({ port: 3000, root: './test' });
await server.listen();
// Collect TAP output
const tap = await collectTAP('http://localhost:3000');
console.log(tap);
// Cleanup
await server.close();my-project/
├── test/
│ ├── utils.js # Shared test utilities
│ ├── component-a.test.html # Test file
│ └── component-b.test.html # Test file
└── package.json
Requires a browser with support for:
- Declarative Shadow DOM (
<template shadowrootmode="open">) - ES Modules
- Import maps
Tested with:
- Chrome/Chromium 90+
- Edge 90+
- Safari 16.4+
Apache-2.0
Kristofer Joseph (@dam)