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

rn-test-forge

v0.1.2

Published

Analyze React Native components and forge unit + functional test cases with locators

Readme

rn-test-forge

Analyze React Native component source and forge the tests it actually needs — unit, component, and device-level E2E — plus a locator map. The interesting part isn't the codegen; it's the planner that decides which tests a piece of functionality warrants.

npx rn-test-forge src/screens/LoginScreen.tsx            # dry run: show the plan
npx rn-test-forge src/screens/LoginScreen.tsx --out __tests__   # write files
npx rn-test-forge src/screens --out __tests__ --ai       # whole dir + LLM edge cases

npx rn-test-forge gaps src/screens/LoginScreen.tsx       # report missing testIDs (exit 1 if any)
npx rn-test-forge gaps src/screens --fix                 # inject stable testIDs in place

npx rn-test-forge diff                                   # plan only for components changed vs HEAD
npx rn-test-forge diff --staged                          # pre-commit: only staged changes
npx rn-test-forge diff --base main --out __tests__       # CI: changed vs main, regenerate suites

Pipeline

source.tsx
   │
   ▼  @babel/parser + traverse
ComponentModel   ── props, useState, handlers, JSX interactions,
   │                testIDs / a11y labels, conditional branches,
   │                async + validation flags
   ▼  planner (deterministic rules)
TestPlan         ── intents: { id, tier, title, rationale, targets }
   │                tier ∈ unit | component | e2e
   ▼  (optional) Claude augmentation — adds edge cases only
   ▼  generators
   ├── <Name>.test.tsx     Jest + @testing-library/react-native
   ├── <Name>.locators.ts  single source of truth for testIDs
   └── <Name>.flow.yaml    Maestro happy-path flow

The planner: how "what test to create" is decided

Each rule maps an observed fact to a test intent. This is the QA knowledge, encoded:

| Observed in the component | Test that gets planned | Tier | |---|---|---| | Component renders | render / smoke | unit | | Required prop (no default, not a callback) | prop drives output | unit | | Optional prop with default | default-path test (prop omitted) | unit | | onPress / onChangeText on an element | interaction test on that testID | component | | Callback prop (onLogin, onSubmit) | asserts callback fires w/ args | component | | cond && <X/> or a ? <A/> : <B/> | both branch paths | component | | .includes/.length/.test/.match in a handler | valid + invalid input boundaries | component | | await / fetch / axios | loading + error state tests | component | | interactive and has an outward callback | end-to-end happy path | e2e |

Deterministic by default — it never guesses. --ai hands the model + plan to Claude and asks only for additional domain edge cases (boundary values, a11y, race conditions); it can't restructure or remove what static analysis authorized. Same trust-by-default / smart-on-demand split as the reducer gate in toklite.

Design decisions worth knowing

  • testID is the contract. Locators come straight from testID / accessibilityLabel in the JSX. If an interactive element has none, that's a real gap — the roadmap flags it (and can auto-suggest one) rather than inventing a brittle path selector.
  • Tiers, not one bucket. A useState toggle is a unit concern; a submit that calls an API is a component concern; a full login is E2E. The planner assigns tier so you don't run a simulator for something a render test covers.
  • Scaffolds with rationale, not blank files. Every generated test() carries the why as a comment and a filled-in arrange/act, leaving only the domain assertion as a TODO. Faster to finish than to start cold, and honest about what the tool can't infer.

Locator gaps (gaps command)

Because locators come straight from testID, an interactive element without one is a real hole — the tests either fall back to brittle queries or can't target it at all. gaps finds those elements and suggests a stable id derived from context, in priority order: accessibilityLabelplaceholder → nested label text → element role. TouchableOpacity wrapping Save Changes becomes save-changes-button; a TextInput with placeholder="Display name" becomes display-name-input.

  • Report exits non-zero when gaps exist, so it drops into CI or a pre-commit hook.
  • --fix injects the suggested testID by splicing at the AST node's offset — no reprint, so your formatting, comments, and quote style survive untouched. It's idempotent (a second run finds nothing) and collision-safe against ids already in the file.

MCP server

forge(), the analyzer, and the gap detector are exposed as MCP tools over stdio, so an agent (or Papio) can request tests and locator fixes inline against live source — no disk round-trip. Every tool takes either inline source (the agent path) or a path (the human/CI path), and none of them write to disk; locator_gaps returns the fixed source as a string for the caller to apply.

| Tool | Purpose | |---|---| | analyze_component | Return the structured ComponentModel (read-only) | | forge_tests | Full pipeline → Jest suite + locator map + Maestro flow as strings | | locator_gaps | Missing-testID report; fix:true also returns injected source |

Register it with any MCP client (e.g. Claude Desktop, or your own agent):

{
  "mcpServers": {
    "rn-test-forge": { "command": "npx", "args": ["-y", "rn-test-forge-mcp"] }
  }
}

Layering note vs. mobile-automation-mcp: that server drives a device; this one reasons about source. An agent flow chains them — forge_tests to author the plan and locators, then the device server to run the resulting Maestro/Detox flow.

Coverage diff (diff command)

Regenerating every test on every commit is noise. diff re-plans only the components you changed, and within each one, marks only the tests your edit actually touched — by parsing git diff --unified=0 into changed line ranges and intersecting them with each feature's AST span.

Edit one line inside handleSubmit and it flags the callback, validation, loading, error, and e2e tests — not the render or prop tests. Rename an input's testID and it flags that input's interaction test plus render — nothing else. A brand-new file comes back fully affected (nothing to diff against), so nothing is silently skipped.

Modes: default compares against HEAD (all uncommitted work); --staged compares the index (for a pre-commit hook); --base <ref> compares against a branch (for CI). Add --out to regenerate suites for just the changed files, --all to see skipped tests too, --json for tooling.

Pre-commit hook (.husky/pre-commit or .git/hooks/pre-commit):

#!/bin/sh
npx rn-test-forge diff --staged --out __tests__ && git add __tests__

Edit-preserving regeneration

Running forge --out a second time merges instead of overwriting, so the tool is safe to run repeatedly (which is what makes the diff pre-commit hook usable). Every generated test carries a hidden marker — // @forge <id> sig:<hash> — recording what the scaffold looked like when generated. On regeneration each block is classified:

  • untouched scaffold (hash still matches) → refreshed to reflect current code
  • human-edited (you filled in a // TODO or changed an assertion) → preserved verbatim; if the underlying feature also changed, it's flagged ⚠ review rather than silently left stale
  • new feature → a new test is added
  • feature removed → the orphaned test is kept and flagged (or dropped with --prune)
  • hand-written test (no marker) → always preserved

The result is idempotent: regenerate as many times as you like and the file is stable — filled-in assertions survive, new tests appear, nothing you wrote is lost. Signatures ignore whitespace/formatting (so Prettier won't trip them) but catch real token changes.

rn-test-forge src/screens/CartScreen.tsx --out __tests__          # merge on re-run
rn-test-forge src/screens/CartScreen.tsx --out __tests__ --prune  # also drop removed-feature tests

Framework & library awareness

Real screens don't render in isolation — they call useNavigation, useSelector, useQuery, useContext. A naive render() throws on mount. The tool detects these dependencies (by hook name and import source, so useQuery from react-query vs Apollo are told apart) and generates the mocks and providers needed to mount:

  • React Navigation → mocks useNavigation/useRoute; route params become a mockRouteParams object; navigation.goBack()/navigate() calls get a navigates … assertion test.
  • Redux → mocks react-redux so useSelector(sel) runs against a mockState you shape (no store wiring).
  • React Query / TanStack → wraps in a real QueryClientProvider (retry off).
  • Apollo → wraps in MockedProvider. SWRSWRConfig. Context<X.Provider>. Zustand → flagged for store seeding.

Everything composes into one renderWithProviders() helper that the generated tests call instead of render(). Components with no such dependencies get a plain render() — no ceremony added where it isn't needed. Each assumption (empty mockState, empty context value, live query fn) is surfaced as a // NOTE: at the top of the file so you know exactly what to fill in.

AI assertion filling (--ai)

With --ai (and ANTHROPIC_API_KEY set), the tool goes beyond scaffolding: it sends the component source plus each // TODO scaffold to Claude and splices back concrete assertions — asserting rendered text, mock calls with arguments, navigation destinations, state changes — using the mocks already in scope (mockNavigate, mockDispatch, mockState, the callback jest.fn()s). It never adds imports or touches the provider setup.

Three properties make it safe:

  • Only TODO blocks are sent — completed scaffolds (render, etc.) are left alone, saving tokens.
  • Signatures reflect the scaffold, not the fill — so an AI-filled test is treated as "edited" by the merger and preserved on later runs. The model fills once; it never rewrites assertions you've reviewed. (A plain regen keeps AI fills; even a second --ai run only fills new TODOs.)
  • Every returned body is validated — it must parse as exactly one test() call or it falls back to the scaffold. No API key, network error, or malformed JSON can break generation; it silently degrades to TODO scaffolds.
export ANTHROPIC_API_KEY=sk-...
rn-test-forge src/screens/OrderScreen.tsx --out __tests__ --ai

Roadmap

  • ~~Locator gap report + --fix~~ ✓ shipped (gaps)
  • ~~MCP server wrapper~~ ✓ shipped (rn-test-forge-mcp)
  • ~~Feature-aware field values~~ ✓ shipped (field-values.js)
  • ~~Coverage diff~~ ✓ shipped (diff)
  • ~~Edit-preserving regeneration~~ ✓ shipped (merge on re-run + --prune)
  • ~~Framework/library awareness~~ ✓ shipped (providers.js — nav, redux, react-query, apollo, swr, context)
  • ~~LLM assertion filling~~ ✓ shipped (--ai fills TODOs; edit-preserving, validated, graceful)
  • Detox generator alongside Maestro (e2e-detox.js slot already reserved).
  • Testability score — report components that are hard to test (side effects in render, no testIDs, deep conditionals).

Layout

bin/cli.js                 CLI (forge + gaps subcommands)
src/analyzer/parse.js      babel config
src/analyzer/extract.js    AST -> ComponentModel   (the hard part)
src/planner/test-plan.js   rules -> TestPlan        (the brain)
src/locators/gap.js        missing-testID detector + offset-based --fix
src/diff/git-diff.js       parse git diff into changed line ranges
src/diff/affected.js       intersect ranges with AST spans -> affected tests
src/merge/merge-tests.js   edit-preserving regeneration (marker + signature merge)
src/generators/            plan -> files (unit-jest, e2e-maestro, field-values, providers)
src/llm/augment.js         optional Claude pass (adds edge-case intents)
src/llm/fill-assertions.js  optional Claude pass (fills TODO assertions)
src/mcp/tools.js           transport-agnostic tool fns (unit-testable)
src/mcp/server.mjs         MCP stdio wiring
examples/                  LoginScreen, SettingsForm + features/ (6 varied)