Skip to content
substrate-systemPublic

About

The easiest way to run tests in a browser from the command line.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

tapout

tests types module semantic versioning Common Changelog license

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.

Contents

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, chrome, firefox, webkit,
                        edge (default: chromium). "chrome" and "edge" use
                        the browser already installed on the machine, so
                        they need no `playwright install`.
  -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)
  --auto-finish         Finish after a short quiet period if the tests
                        never signal completion (the pre-0.1.0 default)
  -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 --browser chrome
  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
  cat test.js | tapout --auto-finish

Featuring

  • Cross-browser testing: Run tests in Chrome, Firefox, Safari (WebKit), or Edge
  • Vite compatible: Support for import.meta.env variables
  • Explicit completion: A run ends when the tests say they are done, not after a guess about idle time
  • 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

Install

npm i -D @substrate-system/tapout

Install Playwright Dependencies

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.

Use

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

Ending a run

A run ends in one of four ways:

  1. The tests set window.testsFinished = true. Any other value is ignored.
  2. The output has a top-level TAP plan (1..N, optionally with a directive such as 1..0 # SKIP reason). A plan after the test points (tapzero, tape) ends the run at once. A plan before them ends the run when N test points have been seen, so anything logged or thrown after the Nth test point is not seen. Indented subtest plans are ignored.
  3. An uncaught error or unhandled promise rejection. The run fails.
  4. --timeout expires. The run fails with exit code 1.

When the tests set window.testsFinished, tapout still checks the plan. If a plan promised more test points than ran, the run fails.

The following tapzero example needs no flag, because tapzero prints a trailing plan:

import { test } from '@substrate-system/tapzero'

test('example test', (t) => {
  t.ok(true)
})

Hand-written TAP with no plan must end by setting the flag:

console.log('TAP version 13')
console.log('ok 1 - the page has a body')
window.testsFinished = true

For suites that can do neither, pass --auto-finish to keep the old behavior.

Migrating from 0.0.x

Before 0.1.0, tapout ended a run after a short quiet period. It now waits for one of the endings above.

A suite that prints a TAP plan, or sets window.testsFinished, needs no change.

A suite that does neither now runs until --timeout and fails. Fix it by setting window.testsFinished = true at the end, or by printing a plan, or by passing --auto-finish to keep the old behavior.

Library users: runTestsInBrowser takes autoFinish: true for the same old behavior.

Vite environment variables

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 - false
  • import.meta.env.MODE - "test"
  • import.meta.env.BASE_URL - "/"
  • import.meta.env.SSR - false

Example

// 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'

CI

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

# ...

Github Actions

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.

Generate HTML reports

By default writes to stdout.

cat ./test/index.js | npx tapout --reporter html > index.html
open index.html  # View the generated report

HTML Summary

  • --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, --browser

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 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, --timeout

Pass in a different timeout value. The default is 5 seconds.

If the tests have not ended when the timeout expires, tapout exits 1. Without --auto-finish, the error says no explicit completion was seen.

cat test.js | npx tapout --timeout 5000

Errors: tapout automatically detects test failures from:

  • Unhandled promise rejections
  • Uncaught exceptions
  • Calls to console.error in the page
  • TAP "not ok" results
  • A TAP plan (1..N) mismatch (only when tapout auto-finishes)

Words in the output do not count. A passing line such as ok 1 - shows Failed state passes. The browser's own "Failed to load resource" errors (for example, a 404 for an image) are ignored.

Tests will exit with code 1 if any errors are detected, which is good for CI/CD pipelines.

--auto-finish

--auto-finish restores the pre-0.1.0 behavior. If the tests never signal completion, the run ends after a short quiet period with no output.

The quiet period is 20% of --timeout, clamped to between 0.5 and 3 seconds.

Use it only for suites that cannot print a plan or set window.testsFinished, because a suite that stalls and then fails can exit 0 under it.

For example:

cat test.js | npx tapout --auto-finish

-r, --reporter

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 format
  • html - 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:

  • 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 --outdir nor --outfile is specified, HTML output is sent to stdout

Pass in HTML

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.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.


GitHub Pages Integration

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

Accessibility Testing with Axe

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 specific WCAG rules

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')
})

Disable specific rules

await assertNoViolations(t, {
  rules: {
    'color-contrast': { enabled: false }  // Skip incomplete styles
  }
})

Component testing

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()
})

Example Tests

Write tests for the browser environment.

Tip

tapzero prints a TAP plan when it finishes, which ends the run. With a library that prints no plan, end the tests explicitly with window.testsFinished = true.

// test/index.ts
import { test } from '@substrate-system/tapzero'

test('example', t => {
    t.ok(document.body, 'should find a body tag')
})

Run the tests on the command line.

npx esbuild ./test/index.ts | npx tapout

HTML report

# HTML reporter examples  
npm run test:simple -- --reporter html     # Generate HTML report

Test

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/

About

The easiest way to run tests in a browser from the command line.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages

Generated from nichoth/template-ts