The easiest way to run tests in a browser from the command line. Just pipe some JS into this command. A spiritual successor to tape-run.
This uses playwright under the hood.
Usage: tapout [options]
Options:
-t, --timeout <ms> Timeout in milliseconds (default: 5000)
-b, --browser <name> Browser to use: chromium, firefox, webkit, edge (default: chromium)
-r, --reporter <name> Output format: tap, html (default: tap)
--outdir <path> Output directory for HTML reports (default: current directory)
--outfile <name> Output filename for HTML reports (default: index.html)
--html <path> Serve a custom HTML fixture as the test page (for web components)
-h, --help Show this help message
Examples:
cat test.js | tapout --timeout 5000
cat test.js | tapout --browser firefox
cat test.js | tapout -b webkit -t 3000
cat test.js | tapout --browser edge
cat test.js | tapout --reporter html
cat test.js | tapout --reporter html --outdir ./reports
cat test.js | tapout --reporter html --outfile my-test-results.html
cat test.js | tapout --html ./fixture.html
import.meta.env variablesnpm i -D @substrate-system/tapout
After installing this, you will need to install dependencies for Playwright also.
npx playwright install --with-deps
You can skip this step entirely if you use --browser chrome or
--browser edge. Those launch the Chrome or Edge that is already on the
machine, so there is no browser to download. See
Github Actions.
Pipe some Javascript to this command.
cat ./test/index.js | npx tapout
Use shell redirection
npx esbuild --bundle ./test/index.ts | npx tapout | npx tap-spec
window.testsFinishedThe test browser will automatically close within few seconds of no activity.
To explicitly end the tests, set a property on window.
import { test } from '@substrate-system/tapzero'
test('example test', (t) => {
t.ok(true)
})
test('all done', () => {
// This will cause the tests to exit immediately.
// @ts-expect-error tests
window.testsFinished = true
})
Vite environment variables, like import.meta.env.DEV are defined, so your
tests wont break if you use them in your application code.
import.meta.env.DEV - true (tests run in development mode)import.meta.env.PROD - falseimport.meta.env.MODE - "test"import.meta.env.BASE_URL - "/"import.meta.env.SSR - false// Your Vite app code can use these environment variables
if (import.meta.env.DEV) {
console.log('Running in development mode')
}
const apiUrl = import.meta.env.DEV ?
'http://localhost:3000/api' :
'https://production.api.com'
After npm install, you will need to do an npx playwright install, unless
you are testing in Chrome or Edge only. See
Github Actions for how to skip the download.
For example, in Github CI,
# ...
- name: npm install, build
run: |
npm install
npm run build --if-present
npm run lint
npx playwright install --with-deps
env:
CI: true
# ...
npx playwright install downloads a browser on every CI run, which is
usually the slowest step in the job.
The Github Actions Ubuntu runners ship with Google Chrome and Microsoft
Edge already installed. Playwright can launch an installed browser instead
of its own bundled build, with channel.
The workflow becomes this (no install step):
name: tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22.x
# No `playwright install` -- Chrome is already on the runner.
- name: Install & build
run: |
npm install
npm run build
- name: Test
run: cat ./test/index.js | npx tapout --browser chrome
The engine is the same one your users have, and it is the same locally as in CI, as long as you have Chrome installed.
If you do test in Firefox or WebKit, you still need those, but you can name just the ones you want rather than downloading all of them:
- run: npx playwright install --with-deps firefox webkit
This is the same trick as passing launchOptions: { channel: 'chrome' } to
the Playwright provider in Vitest browser mode.
By default writes to stdout.
cat ./test/index.js | npx tapout --reporter html > index.html
open index.html # View the generated report
--reporter html with no other options -> output HTML to stdout--reporter html --outfile filename.html -> save to filename.html in
current directory--reporter html --outdir ./reports -> save to ./reports/index.html--reporter html --outdir ./reports --outfile custom.html -> save to
./reports/custom.html-b, --browserPass in the name of a browser to use. Default is chromium.
| name | what it launches | needs playwright install? |
|---|---|---|
chromium |
Playwright's bundled Chromium | yes |
chrome |
the Google Chrome on the machine | no |
firefox |
Playwright's bundled Firefox | yes |
webkit |
Playwright's bundled WebKit | yes |
edge |
the Microsoft Edge on the machine | no |
cat test.js | npx tapout --browser firefox
chrome and edge are launched by Playwright channel, meaning they use
a browser that is already installed rather than one Playwright downloads.
That makes them a good default in CI, where the download is pure overhead.
They do require that browser to actually be present, so chromium remains
the default.
cat test.js | npx tapout --browser chrome
-t, --timeoutPass in a different timeout value. The default is 5 seconds.
The timeout respects the auto-finish behavior:
-t flag): Auto-finish triggers after a short
delay (1-2 seconds) when no test activity is detected-t): Auto-finish uses 80% of the specified
timeout, giving tests more time to complete naturallycat test.js | npx tapout --timeout 5000
Errors: tapout automatically detects test failures from:
Tests will exit with code 1 if any errors are detected, which is good for CI/CD pipelines.
-r, --reporterChoose the output format. Default is TAP.
For HTML output, you will want to redirect stdout to a file.
cat test.js | npx tapout --reporter html > test-output.html
Available reporters:
tap - TAP output (default) - Standard Test Anything Protocol formathtml - Generate an HTML report file with beautiful, responsive design# Generate HTML and output to stdout
cat test.js | npx tapout --reporter html > my-report.html
# Use TAP output (default)
cat test.js | npx tapout
# Customize output location
cat test.js | npx tapout --reporter html --outdir ./reports
cat test.js | npx tapout --reporter html --outfile my-test-results.html
cat test.js | npx tapout --reporter html --outdir ./reports --outfile custom-report.html
The HTML reporter generates an index.html file by default with:
Output Control:
--outdir <path> - Specify where to save the HTML report
(default: current directory)--outfile <name> - Specify the filename for the HTML report
(default: index.html)--outdir nor --outfile is specified, HTML output is sent
to stdoutUse option --html to pass in an HTML file instead of JS.
By default tapout serves a minimal empty page.
If you need to test, for example, a web component that depends on having specific markup in the page, this is how to do it. The fixture's directory (where the HTML file is located) is treated as a static root dir for relative links.
A bare fragment (no <!doctype>/<html>) is wrapped in a minimal document
for you.
fixture.html:
<!doctype html>
<html>
<body>
<my-greeting data-name="world"></my-greeting>
<script type="module" src="./my-greeting.js"></script>
</body>
</html>
Run it (assertions still come from stdin):
cat test.js | tapout --html ./fixture.html
The markup is parsed first, then my-greeting.js registers the component and
the browser upgrades the existing element before your assertions run.
Two caveats: tapout reserves the /__tapout/ URL prefix for its harness and
test bundle, so a fixture asset under that path would be shadowed; and harness
injection finds the last </body> by string match, so a literal </body>
inside a comment or string in the fixture would break it.
The generated HTML file is self-contained and can be easily hosted on GitHub Pages or any static hosting service. Simply commit the HTML file to your repository.
# Example CI workflow
npm test 2>&1 | npx tapout --reporter html --outfile test-results.html
git add test-results.html
git commit -m "Update test results"
git push
See Axe.
You can import some utilities from tapout:
import { test } from '@substrate-system/tapzero'
import {
assertNoViolations,
assertWCAGCompliance
} from '@substrate-system/tapout/axe'
test('page has no accessibility violations', async (t) => {
document.body.innerHTML = `
<main>
<h1>Welcome</h1>
<button>Click me</button>
<img src="test.jpg" alt="Test image" />
</main>
`
await assertNoViolations(t)
})
test('form meets WCAG AA compliance', async (t) => {
document.body.innerHTML = `
<form>
<label for="username">Username</label>
<input id="username" type="text" />
<label for="password">Password</label>
<input id="password" type="password" />
<button type="submit">Submit</button>
</form>
`
await assertWCAGCompliance(t, 'AA')
})
test('can test specific elements', async (t) => {
document.body.innerHTML = `
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
`
const nav = document.querySelector('nav')
await assertNoViolations(t, {
context: nav
}, 'navigation should be accessible')
})
test('flexible WCAG level testing', async (t) => {
document.body.innerHTML = `<div>Content</div>`
// Letter levels
await assertWCAGCompliance(t, 'AA')
// Direct tag names
await assertWCAGCompliance(t, 'wcag2a')
// Multiple tags (useful for testing specific WCAG versions)
await assertWCAGCompliance(t, ['wcag2a', 'wcag21a'])
})
test('cleanup', () => {
// @ts-expect-error browser global
window.testsFinished = true
})
test('check color contrast only', async (t) => {
document.body.innerHTML = `<div style="color: #333; background: #fff;">
Content
</div>`
await assertNoViolations(t, {
runOnly: { type: 'rule', values: ['color-contrast'] }
}, 'should pass color contrast')
})
await assertNoViolations(t, {
rules: {
'color-contrast': { enabled: false } // Skip incomplete styles
}
})
import { MyComponent } from '../src/components/MyComponent.js'
test('MyComponent is accessible', async (t) => {
const container = document.createElement('div')
document.body.appendChild(container)
// Render your component
const component = new MyComponent()
component.mount(container)
// Test accessibility
await assertNoViolations(t, { context: container })
container.remove()
})
Write tests for the browser environment.
End the tests explicity with window.testsFinished = true.
Else they time out naturally, which is ok too.
// test/index.ts
import { test } from '@substrate-system/tapzero'
test('example', t => {
t.ok(document.body, 'should find a body tag')
})
test('all done', t => {
// @ts-expect-error explicitly end
window.testsFinished = true
})
Run the tests on the command line.
npx esbuild ./test/index.ts | npx tapout
# HTML reporter examples
npm run test:simple -- --reporter html # Generate HTML report
Run the tests for this module. See the test/ directory.
npm test
/ed3d-plan-and-execute:execute-implementation-plan /Users/nick/code/tapout/docs/implementation-plans/2026-06-29-html-option/ /Users/nick/code/tapout/