npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

qa-capture

v0.2.3

Published

Proof that a human ran your change, on the pull request: record a manual Android test session and attach it as evidence

Readme

qa-capture

Proof that a human ran your change, on the pull request.

Site and docs: https://qa-capture.pages.dev

You already test a change by hand before you merge it. qa-capture watches that session from outside the app and turns it into evidence a reviewer can trust: every screen, every tap, and every control that did nothing, attached to the pull request as screenshots with a step-by-step record behind them.

It exists because reviewers can no longer tell whether anyone ran a change. Pull requests got larger and more of them are written with an AI, and agents now post their own screenshots, which is the one kind of evidence a reviewer should not take on trust. A recorded human session is different in kind: taps are read from the device's own input layer, the one scripted adb shell input bypasses, so a session whose screen changed with no touches recorded is marked partial rather than presented as a person's work.

What it is not:

  • Not an agent that tests for you. That space is crowded and mostly free. The person drives; the tool records.
  • Not an AI that decides whether the app works. It reports which checklist items a session reached. Whether they passed is a human verdict, and nothing in this codebase can write one.
  • Not snapshot testing. Previews show components in isolation. This shows the flow after three taps, with whatever was really on screen.
  • Not a QA-team tool yet. Android, tethered to an emulator or a phone, for the developer opening the pull request. Nothing goes into your app.

Run it from your app repo

The CLI reads the repo it is run in to link the session to a branch and pull request, and reads the device to identify the build. Both are best-effort: a session recorded outside a repo is still a valid session, it just cannot say which change it was testing.

With one device attached it is used. With a phone beside the emulator, pass --device <serial> (or set ANDROID_SERIAL) to say which; otherwise the first device adb lists is used and a warning names it.

Install

npm install -g qa-capture     # or: npx qa-capture <command>
qa-capture doctor             # check what is missing before you need it

You also need:

  • adb — found automatically inside a standard Android Studio install, so usually nothing to do. Set ANDROID_HOME if yours lives somewhere unusual.
  • ffmpeg and ffprobe, 5.1 or newer — on PATH, or point QA_FFMPEG_DIR at a folder containing them. An older ffprobe reports no frame timestamps, which would turn every tap into a no-op; doctor and start refuse it.
  • gh (recommended) — the GitHub CLI, 2.99 or newer. It finds the pull request for your branch and posts the comment with screenshots attached. Without it you can still record: pass --pr <n> and post the saved comment.md yourself. Install with winget install --id GitHub.cli (Windows) or brew install gh (macOS), then run gh auth login. Nothing here installs it or logs in for you.
  • ANTHROPIC_API_KEY (optional) — only for checklist generate and coverage. Recording works without it.

qa-capture doctor checks all of these and says what to do about each. Run it first: a missing dependency discovered halfway through a recording costs the whole session, and you cannot ask someone to tap through it again.

Commands

qa-capture doctor       check that everything needed is present
qa-capture start        record until Ctrl-C, then write the session
qa-capture comment      put the session on the pull request, screenshots attached
qa-capture checklist    the pull request's checklist (list, add, generate, screens, pass)
qa-capture coverage     work out which checklist items the session reached
qa-capture view         build the session page locally
qa-capture publish      optional: give that page a URL on a side branch

Anything that sends data somewhere prints what it would send and does nothing until you add --send, --push or --post.

After a session, build the page with qa-capture view and ask one question: do these steps describe what you did? Everything downstream rests on that.

Tests

The capture pipeline fails quietly rather than loudly: every bug found so far produced a plausible-looking session with wrong evidence, and none of them threw. So each test in tests/ encodes a defect that actually shipped, and each was checked by reverting the fix and confirming the test fails.

The step detector, gesture parser and clock anchoring are pure functions, so they run without a device or a browser.

Development

Node 22 (.nvmrc). Clone, then:

npm ci
npm run typecheck
npm test
npm run build

Run the CLI from source without building: npm run qa-capture -- doctor. CI runs the same three checks on Ubuntu, macOS and Windows. Everything that needs a device is recorded by hand in docs/emulator-validation.md, one numbered round per session; docs/mac-testing.md is the checklist for the macOS pass.

Generating checklist items

checklist generate reads the diff between your branch and its base and proposes things worth trying by hand. Like coverage, it prints what it would send and does nothing without --send.

Suggestions are filtered before you see them, because the way this feature fails is slop rather than silence. An item is dropped if it is known filler ("verify the app doesn't crash", "smoke test the changes") or if it mentions nothing the diff actually touched - that second check catches advice that would fit any pull request ever opened. At most five survive, and what was discarded is printed rather than quietly replaced.

Returning nothing is a valid outcome. Better none than filler.

The tool's own sessions/ and checklists/ folders are excluded from the diff it reads - add them to your .gitignore too, so captured evidence does not end up in your app's history.

Coverage matching sends screenshots off your machine

coverage is the only command that transmits anything, and what it transmits is pictures of your staging app. It prints what it would send and does nothing without --send. If that data cannot leave your network, do not use it: the matcher is an interface (CoverageJudge), so a self-hosted model replaces one file and nothing else.

It answers only whether the session reached an item, never whether the app worked. Every failure resolves to no_evidence - a model error, an invented step id, a claim citing nothing - because a false tick is worse than a missing one.

Screens the change touched

The diff says which screens a pull request touches: the Activity and Fragment classes it edits, the view ids and test tags it adds, the strings a person will read. qa-capture checklist screens prints that list. While recording, the producer follows which screen the device is showing - the accessibility event stream, logcat, and the Fragment stack sampled once a second - and at the end every step is labelled with its screen. Steps on a screen the change touched are flagged, lead the screenshots in the comment, and give the comment its first line. The moment you reach one of those screens the tool takes a lossless screenshot on its own and prints a line so you know it did; --notify and --buzz add an on-device notification and a short buzz. --highlight goes one step further: it reads the accessibility tree at each match and outlines, on the screenshot, the element whose id or string the diff touched - so the reviewer sees not just the screen but the exact control that changed. Reading the tree pauses the live stream for a few seconds per shot, which is why it is opt-in; the shot is then taken after the read, so a screen that was still loading when it appeared is captured with its content. The comment reads:

**Screens this change touched** · 2 of 3 reached
- `CheckoutFragment` — reached at stp_003, stp_004
- “Coupon code” — reached at stp_005
- `SettingsActivity` — not reached in this session

Checklist items get the same treatment. An item whose words name a screen in the diff is marked reached when the session was on that screen, with no model involved and nothing leaving the machine. Reached means the screen was on display, nothing more: whether the item worked is still for a person, or for coverage --send if you ask it.

What stays private: on-screen strings are matched, never stored. Only a matched string or a class name ends up in the session, and typed text is filtered out on the device before anything is written. Compose apps expose their strings but not their navigation routes, so they match on text.

Flutter is different: its event stream carries the app-bar title and nothing else, while every body string and every Semantics(identifier:) lives only in the accessibility tree. So for a Flutter app the tool reads the whole tree after each change on screen and matches on that instead - strings and identifiers alike, live and for the labelling afterwards. It is on by default when the package ships libflutter.so; --tree-sample turns it on for any app and --no-tree-sample turns it off. Each read pauses the live stream for about two seconds, during which a screen change is noticed by the next read rather than the instant it happens; a tree read is the app's own strings only, never what someone typed into a field. One constraint: the stream holds the device's single accessibility client, so Maestro, Appium or an agent cannot drive the device during a recording.

iOS Simulator

The same auto-capture, on the iOS Simulator, on a Mac:

qa-capture start --platform ios --pr 42 --highlight

It records the Simulator's screen with simctl io recordVideo, reads the accessibility tree about once a second with AXe (axe describe-ui), and the moment a screen the diff touched is showing - its strings or accessibilityIdentifiers, since iOS has no screen class names to match - takes a lossless simctl io screenshot and outlines the element. Steps come from the video; they carry no taps, so there is no dead-control detection on iOS and the session says so by leaving interaction empty. Needs Xcode, a booted simulator and AXe on PATH; doctor checks all three. Physical iPhones are not supported yet.

Posting to the pull request

qa-capture comment            # prints the comment, saves sessions/<id>/comment.md
qa-capture comment --post     # puts it on the pull request

comment prints by default and only posts with --post, since it writes in public under your name. Posting goes through the GitHub CLI, so this tool never handles a token, and a second run updates the existing comment rather than piling up (--new forces a fresh one).

Screenshots are attached, not linked. GitHub strips data: URIs from comment markdown, so the CLI uploads a sample of the session's screens with gh --attach (gh 2.99 or newer). They render inline on private repositories too, and nothing is written into your app's git history. Each screen appears as a small preview that opens at full size. Six are attached by default, spread across the session; --screenshots <n> changes that, up to 24, and --no-screenshots posts the summary alone. An older gh posts the summary and says why.

Attachments work on GitHub.com and GitHub Enterprise Cloud. On Enterprise Server the comment goes up without them.

If the pull request was not resolved when you recorded - no gh yet, or the PR was opened afterwards - pass --pr <n> once. It is saved into the session, so later commands find it on their own.

Everything that could stop a post is checked before anything is sent, and the fix is printed next to it: gh missing, not logged in, too old to attach, no pull request. qa-capture doctor runs the same checks up front, and start says at the top of a recording if the pull request could not be found, so you can fix it before you drive the app rather than after.

Publishing the session page (optional)

view builds a self-contained HTML page of the whole session. Screenshots no longer need publishing to show on the pull request, so this only matters if you want reviewers to have the full screen-by-screen page as well.

There is nowhere to serve it from yet, so publish pushes the page onto an orphan branch of the app's own repository - qa-capture-sessions, sharing no history with your code - through a temporary git worktree, so your branch and working tree are never touched. It renders once GitHub Pages is enabled for that branch.

qa-capture view
qa-capture publish                 # says what it would push
qa-capture publish --push          # pushes the page
qa-capture comment --post --session-url <page url>

This is a stopgap. The page inlines every frame, so pushing it puts screenshots of a staging app where anyone who can read the repository can see them. Fine for a throwaway project you are exercising the flow against; not fine for anything real. Self-hosting is on the roadmap for exactly this reason.

The checklist lives on the pull request

Items are written from the diff before testing starts, so they cannot live in a session that does not exist yet, and one pull request collects several sessions over its life. Items and their verdicts are stored per PR under checklists/; a session records only coverage referring to them by id.

Coverage is observed by the tool. The verdict is set by a person, and nothing in this codebase can write one.

Pass --pr <n> when the pull request cannot be resolved from the branch (no GitHub CLI installed, for instance).

Layout

src/
  session/          the contract - schema (zod), ids, writer
  producers/adb/    device probe, getevent capture, screenrecord, clock, app id
  context/          git repo, branch, PR number
  pipeline/         frame diff, step detection, frame extraction
  viewer/           session -> self-contained HTML page
  report/           session -> pull request comment
  publish/          session page -> a branch a reviewer can reach (optional)
  coverage/         which checklist items a session reached
  cli/              one command, several subcommands

producers/adb may import from session/, never the reverse. pipeline/ and viewer/ only ever see session objects. An SDK producer later is a new producer, not a rewrite.

Environment

  • QA_FFMPEG_DIR — directory containing ffmpeg/ffprobe
  • QA_DEBUG=1 — print raw frame PTS per step
  • QA_KEEP_VIDEO=1 — keep capture.mp4 for diagnosis

State

Validated on an emulator only. docs/emulator-validation.md records what was proven, what the numbers were, and what is still unproven — scrolls beyond a basic check, fast tapping, text entry, real apps, and physical devices.

A note on the sync marker

At the start of every session the tool briefly opens and closes the notification shade. This is deliberate: it pins the video clock to the touch clock, which is what lets a screen change be attributed to the tap that caused it. It runs before you start testing, does not touch the app under test, and leaves the screen exactly as it found it. Its own steps are dropped from the session.

If it fails, capture continues and the alignment is estimated instead — the run prints a warning, and attribution may be less reliable.