reacttutorial
Tutorial: testing React components headlessly
No DOM, no browser, no Vite dev server — just react-test-renderer, which is exactly how the adapter's own tests verify this behavior.
The full component tree
import { RBACClient } from 'rbac-fs/client';
import { Can, RbacProvider, usePermission } from 'rbac-fs/react';
function ApproveButton() {
return (
<Can I="approve" a="invoice" fallback={<span>You can't approve invoices</span>}>
<button>Approve invoice</button>
</Can>
);
}
function ImperativeCheck() {
const can = usePermission();
return can('invoice', 'approve') ? <p>Imperative check: allowed</p> : <p>Imperative check: denied</p>;
}
function App({ client }: { client: RBACClient }) {
return (
<RbacProvider client={client}>
<ApproveButton />
<ImperativeCheck />
</RbacProvider>
);
}Render it with a permissive client
import { act, create } from 'react-test-renderer';
const client = new RBACClient({
user: { id: 'u1' },
permissions: [{ resource: 'invoice', actions: ['approve'] }],
});
let renderer;
act(() => {
renderer = create(<App client={client} />);
});
console.log(JSON.stringify(renderer.toJSON(), null, 2));
// renders the real <button>Approve invoice</button> and "Imperative check: allowed"Render it again with a client that has nothing
const otherClient = new RBACClient({ user: { id: 'u2' }, permissions: [] });
let deniedRenderer;
act(() => {
deniedRenderer = create(<App client={otherClient} />);
});
console.log(JSON.stringify(deniedRenderer.toJSON(), null, 2));
// renders the fallback <span> and "Imperative check: denied" — same tree, different clientTwo renders, two different RBACClient instances, same component tree — this is the cleanest way to prove your permission-gated UI actually branches correctly, and it's fast enough to run in CI on every commit.
Verified against examples/12-react-usage.tsx, runnable with node --import tsx.