Testing components#

Janux components test without a browser: createInstance gives you the full runtime — intents, guards, effects, sources, events — as plain function calls.

The basics#

import { describe, expect, it } from 'bun:test';
import { createInstance } from 'janux';
import { TaskBoard } from '../src/components/TaskBoard';

it('adds and toggles tasks', async () => {
  const board = createInstance(TaskBoard);

  await board.intents.add({ title: 'Ship v0.2' });
  await board.intents.toggle({ id: board.snapshot().tasks[0].id });
  expect(board.snapshot().tasks[0].done).toBe(true);
  expect(board.derived.remaining).toBe(0);
});

Testing the agent face#

The same pipeline a real agent hits — guards included:

it('clearDone needs human approval from agents', async () => {
  let proposal;
  const board = createInstance(TaskBoard, { onProposal: (p) => (proposal = p) });

  const result = await board.intents.clearDone({}, { origin: 'agent' });

  expect(result.status).toBe('proposal');          // nothing applied yet
  await proposal.execute();                         // the "human" approves
  expect(board.snapshot().tasks).toEqual([]);
});

Invalid input is a test too: expect(board.intents.add({ title: '' })).rejects.toThrow(/below min/).

Effects, sources and settled()#

it('persists after the debounce', async () => {
  const board = createInstance(TaskBoard);

  await board.attach();                              // starts effects & sources
  await board.intents.add({ title: 'x' });
  await board.settled();                             // waits out the 400ms debounce
  // assert against your api mock here
  await board.dispose();
});

attach() is only needed when the test exercises effects, sources or lifecycle — pure intent tests can skip it.

DOM-level tests (when you need them)#

Use happy-dom's GlobalRegistrator with renderToString + boot() to test resume and delegation — see the framework's own boot.test.ts for the idiom. Most component logic never needs this level.