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
- Cross-browser testing: Run tests in Chrome, Firefox, Safari (WebKit), or Edge
- Vite compatible: Support for
import.meta.envvariables - Smart timeout handling: Use custom timeouts or auto-timeout
- Beautiful HTML reports: Generate HTML reports
- TAP compatible: Standard TAP output works with any TAP formatter
- Zero configuration: Just pipe JavaScript into this command
- CI/CD friendly: Proper exit codes
npm i -D @substrate-system/tapoutAfter installing this, you will need to install dependencies for Playwright also.
npx playwright install --with-depsYou 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 tapoutUse shell redirection
npx esbuild --bundle ./test/index.ts | npx tapout | npx tap-specThe 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 chromeThe 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 webkitThis 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 htmlwith no other options -> output HTML to stdout--reporter html --outfile filename.html-> save tofilename.htmlin current directory--reporter html --outdir ./reports-> save to./reports/index.html--reporter html --outdir ./reports --outfile custom.html-> save to./reports/custom.html
Pass 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 firefoxchrome 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 chromePass in a different timeout value. The default is 5 seconds.
The timeout respects the auto-finish behavior:
- With default timeout (no
-tflag): Auto-finish triggers after a short delay (1-2 seconds) when no test activity is detected - With custom timeout (using
-t): Auto-finish uses 80% of the specified timeout, giving tests more time to complete naturally
cat test.js | npx tapout --timeout 5000Errors: tapout automatically detects test failures from:
- Unhandled promise rejections
- Uncaught exceptions
- Console error messages with common error patterns
- TAP "not ok" results
Tests will exit with code 1 if any errors are detected, which is good for CI/CD pipelines.
Choose the output format. Default is TAP.
Note
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.htmlThe HTML reporter generates an index.html file by default with:
- Test summary with pass/fail counts and percentages
- Individual test results with status indicators
- Browser and timing information
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)- If neither
--outdirnor--outfileis specified, HTML output is sent to stdout
Use 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.htmlThe 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 pushSee 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.
Tip
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 reportRun 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/