kahraman gives Storybook and Vitest browser tests a small I.see(…) / I.click(…) actor and a fluent locator DSL built on roles, accessible names, and visible text. Every step runs against your story rendered in a real browser — not a jsdom stand-in — so the test sees what the user sees, and inaccessible UI fails where it should.
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'), 'ada@example.com')
await I.click(button('Continue'))
await I.waitExit(role('status'))
await I.see(heading('Welcome, Ada'))
}) CSS selectors and test IDs describe implementation. kahraman locators describe what a person can perceive: a button named “Continue,” a textbox named “Email,” a heading named “Welcome, Ada.”
await I.see(heading('Sign in'))
await I.fill(role('textbox', 'Email'), 'ada@example.com')
await I.click(button('Continue')) If you can't target an element by its role or name, neither can assistive tech.
The public locator surface stays deliberately narrow. Refine a semantic query without abandoning its intent: wait for it, collect all matches, make a lookup nullable, scope it to a region, or pass Testing Library options.
heading('Dashboard') // getBy
button('Save').within(role('dialog')) // getBy, scoped
role('status', 'Loading').wait() // findBy
role('listitem').all() // getAllBy
role('alert').maybe() // queryBy | kahraman | mode | result |
|---|---|---|
default | getBy* | one element; throws when missing |
.wait() | findBy* | Promise<element> |
.all() | getAllBy* | element[] |
.maybe() | queryBy* | element | null |
.within(scope) | scoped query | same variant, scoped |
.options(...) | query options | same variant |
One actor, initialized from a story context, expresses the whole journey — assertions, interactions, stabilization, and extraction — in causal order.
When a step fails, kahraman appends the complete ✔ / ✖ step trace and
retargets the stack to your story or page actor. Add the optional
kahraman/preview annotation to trim enormous Testing Library
role listings down to the relevant near-misses and a capped DOM excerpt.
// .storybook/preview.ts
import kahraman from 'kahraman/preview'
export default {
...kahraman,
// ...other preview config
}
kahraman is a small intent-level layer over the storybook/test
runtime — it reads only canvasElement and userEvent
from story context, so it stays renderer-agnostic.
jsdom made sense when real browsers were slow and flaky in CI. That bill came down. Vitest browser mode runs your stories in an actual browser cheaply enough to do it by default. jsdom still fits pure logic — but for UI, there's little reason left to assert against a DOM that never lays out, focuses, or paints.
A fake DOM can't catch what a fake DOM can't render. kahraman tests where rendering, focus, and accessibility actually happen.
kahraman ships the generic actor and locator DSL. Add narrowly scoped
page vocabulary with I.extend(…) — seeError(),
selectCountry() — built from the same accessible primitives.
Readable for humans. Writable for agents.
Declarative steps are a format coding agents can follow, and the repo
ships the kahraman-storybook-testing skill.
const withPageError =
(error) => (I) => ({
seeError: async () => {
await I.see(heading(error.title))
await I.see(role('alert'))
await I.see(button('Try again'))
},
})
const I = createActor().extend(
withPageError({ title: 'Something went wrong' }),
)
await I.seeError() Examples, not mandatory templates — adopt only the parts your own Storybook setup supports.
Install kahraman, initialize one actor in a story, and write the next test in the language of roles, names, actions, and visible outcomes.