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

kahraman

v0.3.1

Published

Accessibility-first, codecept-style test actor and locator DSL for Storybook portable stories and Vitest browser mode.

Readme

kahraman

Accessibility-first, codecept-style test actor and locator DSL for Storybook portable stories and Vitest browser mode.

kahraman gives you a small, declarative I.see(...) / I.click(...) actor built on Testing Library queries, plus a fluent locator DSL that biases every assertion toward roles, accessible names, and visible text. It runs inside a Storybook story play function or a Vitest browser test — anywhere you have a canvas and userEvent.

Built and maintained by apphane.

See it in motion

https://github.com/user-attachments/assets/ec192e14-64ab-4d11-b504-82d8d053a400

import { createActor, button, heading, role } from 'kahraman'

const I = createActor()

export const Default = meta.story()
Default.test('signs the user in', async (context) => {
	I.init(context)
	await I.see(heading('Sign in'))
	await I.fill(role('textbox', 'Email'), '[email protected]')
	await I.click(button('Continue'))
	await I.waitExit(role('status'))
	await I.see(heading('Welcome, Ada'))
})

Why accessibility-first

Every locator resolves through Testing Library's accessibility tree — by role, accessible name, heading, link, button, or visible text — before you ever reach for a lower-level selector. Tests written this way assert what a user (and a screen reader) actually perceives, and they surface real accessibility gaps: if you can't target an element by its role or name, neither can assistive tech. kahraman deliberately keeps the public locator surface small so the path of least resistance is also the accessible one.

Install

npm install --save-dev kahraman
# or: pnpm add -D kahraman

storybook (which provides storybook/test, and with it Testing Library and userEvent) is a peer dependency — you already have it. kahraman ships ESM only and is renderer-agnostic: it reads only canvasElement and userEvent from the story context, so it works with the React, Vue, and Svelte renderers alike.

Or vendor the source with jsrepo

Prefer to own the code? kahraman is also published as a jsrepo registry, so you can copy the source straight into your project and adapt it:

# the actor + locator DSL
npx jsrepo add github/apphane-dev/kahraman/actor

# the opt-in diagnostics preview annotation
npx jsrepo add github/apphane-dev/kahraman/preview

# include the unit tests / example page actor too
npx jsrepo add --with test --with example github/apphane-dev/kahraman/actor

The full source also lives in src/ inside the published npm tarball, so it is available for reference even when you install the package normally.

AI agent skill

The repository includes kahraman-storybook-testing, a portable Agent Skill for writing and reviewing these Storybook tests. Install it with skills:

npx skills add apphane-dev/kahraman --skill kahraman-storybook-testing

Or install directly from a checked-out copy:

npx skills add . --skill kahraman-storybook-testing

The skill lives at skills/kahraman-storybook-testing/SKILL.md, so npx skills add apphane-dev/kahraman --list can discover it from the GitHub repository.

Reference application setups

These are examples rather than required infrastructure; adopt only the parts supported by your project's Storybook setup.

Usage

Create and initialize an actor

import { createActor } from 'kahraman'

const I = createActor()
// Inside a story play / test, before any actor call:
I.init(context) // context: the Storybook StoryContext (any renderer)

createActor() returns a fresh actor with its own step trace. Call I.init(context) first — it wires up the canvas and resets the trace.

To make clicks easier to follow when manually playing a story, configure an explicit delay. The default is 0, so automated tests are not slowed down:

const I = createActor({
	clickDelay: navigator.webdriver ? 0 : 500,
})

Keeping the environment check in the consumer makes the pacing policy visible and lets each Storybook choose how it distinguishes manual and automated runs.

Locators

Build locators with role, text, and the heading / button / link shorthands, then refine them fluently:

import { role, text, heading, button, link } from 'kahraman'

role('textbox', 'Email') // by role + accessible name
heading('Dashboard') // role('heading', 'Dashboard')
text(/items? found/) // by visible text (string or RegExp)

link('Storage').options({ current: 'page' }) // extra Testing Library options
button('Save').within(role('dialog')) // scope to a container
role('listitem').all() // all matches (for counts)
role('status', 'Loading').wait() // findBy* (async)
role('alert').maybe() // queryBy* (nullable)

| Transition | Query mode | Returns | | ------------ | ------------------- | -------------------- | | (default) | getBy* | one element (throws) | | .wait() | findBy* | Promise<element> | | .all() | getAllBy* | element[] | | .maybe() | queryBy* | element \| null | | .within(s) | scope to s | same variant, scoped | | .options() | merge query options | same variant |

.within(scope) accepts a locator, an HTMLElement, or 'global' (the whole document body).

Actor methods

// Assertions
await I.see(role('alert')) // present in the document
await I.dontSee(button('Delete')) // absent
await I.waitExit(role('status')) // wait for it to disappear (stabilization)
await I.seeInField(role('textbox', 'Email'), '[email protected]')
await I.seeChecked(role('checkbox', 'Remember me'))
await I.seeDisabled(button('Save'))
await I.seeAttribute(link('Docs'), 'target', '_blank')
await I.seeNumberOfElements(role('listitem').all(), 3)

// Interactions
await I.click(button('Save'))
await I.fill(role('textbox', 'Email'), '[email protected]')
await I.clear(role('textbox', 'Email'))
await I.selectOption(role('combobox', 'Country'), 'Portugal')
await I.press('{Enter}')

// Scoping
await I.within(role('main'), async () => {
	await I.see(heading('Details'))
})

// Extraction
const title = await I.grabTextFrom(heading())
const rows = await I.grabTextFromAll(role('row').all())

// Resilience
await I.retryTo((n) => I.see(text(`Attempt ${n}`)), 3, 200)
if (await I.tryTo(() => I.see(role('dialog')))) {
	/* optional */
}

// Soft assertions — collect failures, report all at once
await I.hopeThat(() => I.see(heading('A')))
await I.hopeThat(() => I.see(heading('B')))
I.hopeThat.noErrors() // throws an AggregateError if any failed

Extending with page actors

Keep reusable, page-level expectations out of your stories with I.extend(...):

import { button, heading, role, text, type BaseActor } from 'kahraman'

const withPageError =
	(error: { title: string; description?: string }) => (I: Pick<BaseActor, 'see'>) => ({
		seeError: async () => {
			await I.see(heading(error.title))
			if (error.description) {
				await I.see(text(error.description))
			}
			await I.see(role('alert'))
			await I.see(button('Try again'))
		},
	})

const I = createActor().extend(withPageError({ title: 'Something went wrong' }))
// ...later: await I.seeError()

See examples/pageActor.ts for a fuller illustration.

For a real-world app that exercises kahraman in its Storybook stories, see apphane-dev/karkas.

Storybook Interactions panel

When the story context provides context.step (every Storybook story context does, including the one loaders receive), each actor call is automatically reported as a labeled step. The Interactions panel then reads exactly like your test source — collapsible, codecept-style groups instead of raw Testing-Library calls:

▶ I.seeDetailError()
▶ I.retry()
▶ I.waitExit(role "status")
▼ I.see(heading "Quarterly report")
    within(...).findByRole("heading", { name: "Quarterly report" })
    expect(...).toBeInTheDocument()
▶ I.seeArticleDetail("Quarterly report")

Page-actor methods created with I.extend(...) are steps too, with the base calls they make nested one level deeper — both in the panel and in the failure step trace. Because the tracker is async, extension methods always return a Promise; declare them async (page actors in practice already are).

No setup is needed; outside Storybook (portable stories, plain Vitest) the actor detects the missing step and behaves as before.

Failure diagnostics

When an actor call fails, kahraman augments the error to make the failure readable:

  • Step trace — the message ends with every actor call that ran, /, with locator labels (e.g. ✖ I.see(heading "Welcome")).
  • Retargeted stack — the code frame points at your story / page-actor call site, not kahraman's internals. Internal frames are detected relative to the package's own module URL, so this works whether kahraman is installed or vendored.

Opt-in: tame element-not-found output (kahraman/preview)

Testing Library's getByRole misses dump a listing of every accessible role on the page — hundreds of lines on a full app mount. The kahraman/preview annotation filters that to just the queried role's near-misses and caps the DOM dump. Add it to your Storybook preview:

// .storybook/preview.ts
import kahraman from 'kahraman/preview'

export default {
	...kahraman,
	// ...your other preview config
}

Tune or opt out per story via parameters:

parameters: {
  kahraman: {
    captureRoleListing: false, // keep TL's full role listing (still length-capped)
    maxLength: 4000, // raise the truncation ceiling
    // getElementError: myOwnHandler, // your handler wins entirely
  }
}

Outside Storybook you can wire it up imperatively with configureDiagnostics() from kahraman/preview.

Consumer configuration

Two behaviors are controlled by your Vitest/Storybook setup, not by the package:

  • VITE_TEST_STEPS — set VITE_TEST_STEPS=true (a Vite/Vitest env var) to log each actor step live as it runs, like codeceptjs run --steps. kahraman reads it defensively; when unset (or under plain Node), logging is off.
  • screenshotFailures — failure screenshots are a Vitest browser mode feature. Configure test.browser.screenshotFailures in your own Vitest config to write a screenshot next to the story on failure.

Testing the pure helpers

kahraman's own test suite unit-tests only the Storybook-free logic: locator label construction, step-label formatting, role-listing filtering, message capping, and stack-frame retargeting. The full actor behavior requires a real Storybook browser context and is exercised by consumers' stories — that boundary is intentional. The DOM-touching modules (actor.ts, preview.ts) run only in the browser.

Development

This repo uses the Vite+ / Oxc toolchain. The plain pnpm commands below remain the contributor path. If you use mise, the common workflows are also available as mise run prepare, mise run dev, mise run check, and mise run ci.

| Task | Command | Tool | | ---------- | ------------------ | -------------------------------------- | | Typecheck | pnpm typecheck | tsc (TypeScript 7, native) | | Lint | pnpm lint | oxlint (via vp lint) | | Analyze | pnpm lint:fallow | fallow (dead-code, dupes, health) | | Format | pnpm format | oxfmt (via vp fmt) | | Test | pnpm test | Vitest (via vp test) | | Build | pnpm build | tsdown (Rolldown) → ESM + .d.ts | | Everything | pnpm check | typecheck + lint + fallow + fmt + test |

Formatter and linter configuration lives in the fmt / lint blocks of vite.config.ts (the Vite+ convention), not in separate .oxfmtrc.json / .oxlintrc.json files. Notable engineering decisions are recorded under docs/decisions/ — e.g. why type-aware linting is off by default (0001).

Node, pnpm, and the git-hook runner are pinned with mise (mise install). Git hooks run through hk — configured in hk.pkl, install them once with hk install (pre-commit: format + lint + typecheck + hygiene; pre-push adds fallow).

Publish quality is gated by publint and @arethetypeswrong/cli against the packed tarball, releases run through Changesets, and every commit gets an installable preview via pkg.pr.new.

Acknowledgements

kahraman stands on prior art:

  • CodeceptJS (MIT) — the actor DSL is modelled on its I.see(...) / I.click(...) / hopeThat / retryTo / tryTo style. The concepts and API shape are borrowed; the implementation here is original (no CodeceptJS code is used or redistributed).
  • Testing Library — every locator resolves through its accessibility-first queries.
  • Storybook — the actor runs against the storybook/test runtime (portable stories, Vitest browser mode).

License

MIT © Aleksei Gurianov