# tape-six-puppeteer

> A helper for [tape-six](https://github.com/uhop/tape-six) that runs test files in a headless browser via Puppeteer. Each test file runs in its own iframe in Chromium or Firefox (default Chromium, selectable via `--browser`). The npm package name is `tape-six-puppeteer` and the CLI command is `tape6-puppeteer`.

- Real browser environment: tests run in a headless engine (Chromium or Firefox) with full DOM and browser API access
- Per-context isolation: each test file runs in its own browser context (isolated cookies/storage), in an iframe inside that context's page
- Multiple engines: `--browser` / `TAPE6_BROWSER` selects `chromium` (default) or `firefox`
- Cross-runtime: Node, Deno, and Bun with the same test files
- Drop-in companion for `tape6` (worker-thread runner) and `tape6-proc` (subprocess runner)
- Same configuration format as `tape-six`
- Parallel execution with configurable concurrency
- TAP, TTY (colored), JSONL, and minimal output formats

## Install

```bash
npm i -D tape-six-puppeteer
```

Puppeteer's bundled Chromium is installed automatically via `postinstall`. Firefox is optional — add it with `npm run browser:all` (or `npx puppeteer browsers install firefox`).

## Quick start

Write a test (`tests/test-dom.js`):

```js
import test from 'tape-six'

test('DOM works', t => {
  const el = document.createElement('div')
  el.textContent = 'hello'
  document.body.appendChild(el)
  t.equal(document.body.lastChild.textContent, 'hello', 'element created')
})
```

Run all configured tests via Puppeteer:

```bash
tape6-puppeteer --start-server --flags FO
```

## CLI: tape6-puppeteer

Runs test files in parallel, each in its own browser context inside a headless engine (Chromium by default; Firefox via `--browser`).

```bash
tape6-puppeteer [--flags FLAGS] [--par N] [--browser NAME] [--start-server] [--server-url URL] [--info] [--help] [--version] [tests...]
```

### Options

- `--flags FLAGS` (`-f`) — output control flags (see Supported flags below).
- `--par N` (`-p`) — number of parallel iframes. Default: all CPU cores (via `os.availableParallelism()` or `navigator.hardwareConcurrency`).
- `--browser NAME` (`-b`) — browser engine: `chromium` (default) or `firefox`. Overrides `TAPE6_BROWSER`. Only Chrome is installed by `postinstall`; a missing engine fails the run with an `npx puppeteer browsers install <product>` hint.
- `--start-server` — auto-start `tape6-server` if not already running.
- `--server-url URL` (`-u`) — server URL. Overrides `TAPE6_SERVER_URL` and `HOST`/`PORT`.
- `--self` — prints the absolute path to `tape6-puppeteer.js` and exits. Used in cross-runtime scripts.
- `--info` — prints runtime, reporter, flags, parallelism, and resolved test files, then exits. Does not require a running server.
- `--help` (`-h`) — shows help message with all options and flag descriptions, then exits.
- `--version` (`-v`) — prints version and exits.
- Positional arguments: test file glob patterns. If none given, resolved from configuration.
- Options accept `--flags FO` or `--flags=FO`. The `=` form does not support quoting.

### Examples

```bash
# Run all configured tests
tape6-puppeteer --start-server --flags FO

# Run specific test files
tape6-puppeteer --start-server tests/test-dom.js

# Limit parallelism
tape6-puppeteer --start-server --par 2 --flags FO

# Run on Firefox
tape6-puppeteer --start-server --browser firefox --flags FO
TAPE6_BROWSER=firefox tape6-puppeteer --start-server --flags FO

# Use with already-running server
tape6-puppeteer --flags FO
```

## Cross-runtime usage

`tape6-puppeteer` is a Node CLI by default. For Bun and Deno, use the `--self` flag to get the script path:

```json
{
  "scripts": {
    "test": "tape6-puppeteer --start-server --flags FO",
    "test:bun": "bun run `tape6-puppeteer --self` --start-server --flags FO",
    "test:deno": "deno run -A `tape6-puppeteer --self` --start-server --flags FO"
  }
}
```

## Supported flags

Flags are a string of characters. Uppercase = enabled, lowercase = disabled.

- `F` — Failures only: show only failed tests.
- `T` — show Time for each test.
- `B` — show Banner with summary.
- `D` — show Data of failed tests.
- `O` — fail Once: stop at first failure.
- `N` — show assert Number.
- `M` — Monochrome: no colors.
- `C` — don't Capture console output.
- `H` — Hide streams and console output.

Usage:

```bash
tape6-puppeteer --start-server --flags FO
TAPE6_FLAGS=FO tape6-puppeteer --start-server
```

## Server

`tape6-puppeteer` requires `tape6-server` (from `tape-six`) to serve test files to the browser.

- `--start-server`: auto-starts the server before running tests.
- Without it: the server must already be running. The runner prints instructions if it's unreachable.
- Server URL: `TAPE6_SERVER_URL` env var, or `HOST`/`PORT`, or default `http://localhost:3000`.
- Server endpoints used: `GET /--tests` (test file list), `GET /--patterns?q=...` (filtered file list), `GET /--importmap` (import map).

## Configuration

Configuration is read from `tape6.json` or the `"tape6"` section of `package.json` (same format as `tape-six`):

```json
{
  "tape6": {
    "browser": ["/tests/test-*.html"],
    "tests": ["/tests/test-*.*js"],
    "importmap": {
      "imports": {
        "tape-six": "/node_modules/tape-six/index.js",
        "tape-six/": "/node_modules/tape-six/src/"
      }
    }
  }
}
```

The `importmap` section is served by `tape6-server` at `/--importmap` and injected into each iframe for module resolution.

## Environment variables

- `TAPE6_FLAGS` — flags string (combined with `--flags` CLI argument).
- `TAPE6_PAR` — number of parallel iframes (overridden by `--par`).
- `TAPE6_TAP` — force TAP reporter (any non-empty value).
- `TAPE6_JSONL` — force JSONL reporter (any non-empty value).
- `TAPE6_MIN` — force minimal reporter (any non-empty value).
- `TAPE6_SERVER_URL` — full server URL override (e.g. `http://localhost:4000`). Overridden by `--server-url`.
- `TAPE6_BROWSER` — browser engine: `chromium` (default) or `firefox`. Overridden by `--browser` (precedence: CLI > env > default).
- `HOST` — server hostname (default: `localhost`).
- `PORT` — server port (default: `3000`).
- `TAPE6_GRACE_TIMEOUT` — control channel: ms a worker gets to drain (run cleanup hooks) after a terminate, before its browser context is force-closed (default: `5000`).
- `TAPE6_WORKER_TIMEOUT` — control channel: per-worker wall-clock deadline in ms; on expiry the worker is drained then force-killed (default: `0`, disabled). Both are honored by tape-six's hub control plane (provided by the `^1.10` dependency).

## Architecture

### Entry point

`bin/tape6-puppeteer.js` is the CLI entry point:

- With `--self`: prints its own absolute path and exits.
- Otherwise: delegates to `bin/tape6-puppeteer-node.js`.

### Main CLI (`bin/tape6-puppeteer-node.js`)

1. Parses CLI arguments via `getOptions()` from `tape-six` (`--flags`, `--par`, `--start-server`, `--server-url`, `--info`, positional test patterns).
2. Initializes the reporter via `initReporter()` from `tape-six`.
3. Ensures `tape6-server` is reachable (auto-starts if `--start-server`).
4. Fetches test files from server via `GET /--patterns?q=...` (if patterns given) or `GET /--tests`.
5. Fetches importmap from `GET /--importmap`.
6. Creates a `TestWorker` instance and executes all test files.
7. Reports final results and exits with code 0 (success) or 1 (failures).
8. Kills auto-started server on exit.

### TestWorker (`src/TestWorker.js`)

Extends `EventServer` from `tape-six`. Manages the Puppeteer browser and per-task contexts:

- **`constructor(reporter, numberOfTasks, options)`** — launches one headless browser of the selected engine (`options.browser`, default `chromium`) via `puppeteer.launch({browser})`; the user-facing `chromium` maps to Puppeteer's `chrome` product, Firefox runs over WebDriver BiDi, and `--no-sandbox` is applied to Chromium only.
- **`makeTask(fileName)`** — creates a fresh `BrowserContext` → `Page` for the test (full origin/storage isolation), exposes `__tape6_reporter` and `__tape6_error` on that page, navigates it to the server origin, and runs the test file in an iframe inside it. Returns a task ID. If the browser failed to launch (e.g. the engine isn't installed), it reports a failure so the run exits non-zero rather than reporting a false pass.
- **`destroyTask(id, reason)`** — the control plane. `done` closes the task's context; an abort reason (`failOnce` / `timeout`) drains the running test (posts `tape6-terminate` into the iframe), then force-kills it by closing the context after `graceTimeout`.
- **`cleanup()`** — closes the browser.

Task completion is driven by each page's `close` event, not by a reported event, so a force-killed page (which emits nothing) still completes `close(id)` exactly once.

### Browser engines

The engine is chosen with `--browser <chromium|firefox>` / `-b`, or `TAPE6_BROWSER` (precedence: CLI > env > default `chromium`). The CLI validates the value against `supportedBrowsers` (exported from `src/TestWorker.js` — the single source of truth) and exits with an error on an unknown engine. `TestWorker.#init()` then launches it via `puppeteer.launch({browser})`, mapping the user-facing `chromium` to Puppeteer's launch product `chrome` (a Chromium build, Chrome for Testing) and driving Firefox over WebDriver BiDi. `--no-sandbox` is applied to Chromium only (Firefox launches without it). The per-context model, iframe injection, data plane, and control channel are identical across both engines — only the launched browser differs.

`postinstall` fetches only Chrome; Firefox is installed on demand (`npm run browser:all`, or `npx puppeteer browsers install firefox`). Requesting an engine that isn't installed surfaces a clear `npx puppeteer browsers install <product>` hint and fails the run (a launch failure reports a failure, so the run exits non-zero rather than a false pass).

The sibling package `tape-six-playwright` exposes the same `--browser` / `TAPE6_BROWSER` contract, plus `webkit` (Puppeteer drives only Chromium and Firefox).

### Iframe lifecycle

For `.html` files:
1. Set iframe `src` to the file URL with query parameters (`id`, `test-file-name`, `flags`).
2. The HTML file loads `tape-six` which auto-detects `window.parent.__tape6_reporter`.

For `.js`/`.mjs` files:
1. Create iframe, write an HTML document with `importmap` and a dynamic `<script type="module">`.
2. The script sets `window.__tape6_id`, `window.__tape6_testFileName`, `window.__tape6_flags`.
3. Dynamically appends a `<script>` element pointing to the test file URL on the server.
4. `tape-six` initializes, detects `window.parent.__tape6_reporter`, and uses `ProxyReporter`.

### Event flow

```
iframe tape-six → ProxyReporter → window.parent.__tape6_reporter(id, event)
  → page.exposeFunction → Node.js TestWorker.report(id, event) → reporter
```

### Unsupported files

`.cjs`, `.ts`, `.cts`, `.mts` files are skipped with a warning.

### Worker control channel

`TestWorker` implements the provider side of tape-six's worker control channel (full spec in the tape-six repo's `dev-docs/worker-control-channel.md`). The runner stops a worker through `destroyTask(id, reason)`:

- `done` — the test finished; close its context now.
- `failOnce` / `timeout` (abort) — drain cooperatively first: post `{type: 'tape6-terminate'}` into the test's iframe so `tape-six` arms `stopTest` and fires the abort signal, unwinding the test at its next assertion and running its cleanup hooks. If the test doesn't exit within `graceTimeout` (`TAPE6_GRACE_TIMEOUT`), force-kill it by closing the `BrowserContext` — the real kill an in-page iframe can't perform on itself.

This is the driver-backed browser's answer to "no in-page force-kill": a hung, non-cooperative test that ignores the drain is still stopped, because the Node-side driver closes its context. The cooperative drain relies on the in-iframe `tape6-terminate` listener in tape-six's hub control plane, provided by the `^1.10` dependency floor; the force-kill backstop works with any version.

## Dependencies

- **`tape-six`** — the core test library. Imports: `State.js`, `utils/EventServer.js`, `utils/config.js` (`getOptions`, `initReporter`, `showInfo`), `test.js`, `utils/timer.js`.
- **`puppeteer`** — headless browser automation (Chromium and Firefox). Bundled Chromium installed via `postinstall`; Firefox fetched on demand (`npm run browser:all`).

## Writing tests

Tests are standard `tape-six` tests. See the [tape-six documentation](https://github.com/uhop/tape-six/wiki) for the full API.

```js
import test from 'tape-six'

test('browser APIs', t => {
  t.ok(typeof window === 'object', 'window exists')
  t.ok(typeof document === 'object', 'document exists')
  t.equal(typeof fetch, 'function', 'fetch available')
})

test('DOM manipulation', t => {
  const el = document.createElement('div')
  el.classList.add('test')
  t.ok(el.classList.contains('test'), 'classList works')
})
```

`.html` test files can include their own importmap and inline scripts:

```html
<!doctype html>
<html>
  <head>
    <script type="importmap">
      { "imports": { "tape-six": "/node_modules/tape-six/index.js" } }
    </script>
    <script type="module">
      import test from 'tape-six'
      test('html test', t => { t.ok(true, 'works') })
    </script>
  </head>
  <body></body>
</html>
```

## Links

- Docs: https://github.com/uhop/tape-six-puppeteer/wiki
- npm: https://www.npmjs.com/package/tape-six-puppeteer
- tape-six: https://github.com/uhop/tape-six
- tape-six LLM reference: https://github.com/uhop/tape-six/blob/master/llms.txt
