> ## Documentation Index
> Fetch the complete documentation index at: https://developers.reintersect.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Test blocks, actions, references, events, commands and message actions against a fake Reintersect host, with Vitest, bun test or @effect/vitest.

> Reintersect apps are in developer preview. Names and limits can still change before general
> release.

`@reintersect/sdk/testing` runs your app against an in-memory Reintersect host. The host sends
signed deliveries through your app's real router, stores block state and viewer values, records every
call your code makes to the Reintersect API, and checks every rendered tree with the same validator
Reintersect's server runs, so a tree that fails on the server fails here first, with the same
[message](/blocks/limits). Tests run without a network, a workspace or a running dev server.

Using Effect? `@reintersect/sdk/effect/testing` gives the same host as an Effect service for
`@effect/vitest`; see [Testing](/effect/testing) in the Effect guide.

## Set up a host

This is the test `npm create reintersect` writes:

```ts src/app.test.ts theme={null}
import { createTestHost, textOf } from "@reintersect/sdk/testing";
import { describe, expect, it } from "vitest";
import { app } from "./app";
import { TaskBlock } from "./taskBlock";

const testHost = () => createTestHost(app, { config: { project: "Website" } });

describe("task block", () => {
  it("starts a task and shows its project", async () => {
    const host = testHost();
    const block = await host.insert(TaskBlock, { id: "TASK-2", title: "Review the pricing page" });

    expect(textOf(block.tree)).toContain("Website");

    const result = await host.press(block, "Start");

    expect(result.state).toEqual({ status: "doing" });
    expect(textOf(result.block.tree)).toContain("Mark done");
  });
});

describe("task links", () => {
  it("resolves a task link into a task block", async () => {
    const resolved = await testHost().resolveReference("https://tasks.example.com/tasks/TASK-1");

    expect(resolved).toMatchObject({ access: "granted", title: "Write the launch announcement" });
  });

  it("finds tasks by title", async () => {
    const hits = await testHost().searchReferences("task", "pricing");

    expect(hits.map((hit) => hit.title)).toEqual(["Review the pricing page"]);
  });
});
```

`createTestHost` only accepts a complete app, the same check `reintersect build` makes.

### Host options

| Option | Description |
| - | - |
| `config` | The install config your pieces read. |
| `env` | Environment values as strings, as Reintersect would provide them. |
| `actor` | The member who acts, from `testViewer(name, { memberId?, role? })`. Default: Ada, a member. |
| `install` | Overrides for `{ id, workspaceId, kind: "workspace" \| "dev" }`. |
| `locale`, `timeZone` | Sent in every delivery. |
| `now` | The test clock's start time (Promise host). |
| `validator` | Your own extra tree validator, to run on every render after the built-in check. |

## Blocks

| Method | What it does |
| - | - |
| `render(Block, props, state?)` | Renders without storing, like the playground. |
| `insert(Block, props, { conversationId?, circleId? })` | Inserts a block and returns a `TestBlock` with `id`, `props`, `state`, `revision`, `tree`, `actions`, `final` and `renders`. |
| `press(block, label, input?)` | Clicks the button or menu item with that label, as a member would. Fails if no such control is visible to the actor. |
| `act(block, "actionKey", args?, input?)` | Runs an attached action directly, typed by the action's args and input. |
| `vote(block, "input", value)` | Records the actor's pick for a viewer input. |
| `tallies(block)`, `voters(block, "input", value)` | Reads counts and the member ids behind a value. |
| `search(block, "actionKey", query)` | Runs a `Select` search action. |
| `view(block)` | Views the block now, refreshing it if `staleAfter` passed. |
| `visible(block)` | The tree the current actor sees, with [audience](/blocks/audience-and-lifecycle#audience) applied. |
| `reopen(block)` | Reopens a final block, as the creator or an admin. |
| `block(id)` | Loads a stored block by id. |

`press` and `act` return the result: `state`, `tree`, `notice`, `fieldErrors`, `openUrl`, `final`,
`notify`, `revalidated`, plus the updated `block`.

Switch who acts with `host.as(viewer)`:

```ts theme={null}
import { createReintersectApp } from "@reintersect/sdk/app";
import { approval } from "@reintersect/sdk/recipes";
import { createTestHost, testViewer, textOf } from "@reintersect/sdk/testing";
import { expect, it } from "vitest";

const ExpenseApproval = approval({ type: "expense-approval" });
const app = createReintersectApp({ name: "Expenses" }).addBlock(ExpenseApproval);

const lead = testViewer("Ada");
const teammate = testViewer("Grace");

it("lets the finance lead approve", async () => {
  const host = createTestHost(app, { actor: lead });
  const block = await host.insert(ExpenseApproval, { title: "New laptop", approvers: [lead.memberId] });

  expect(textOf(await host.as(teammate).visible(block))).not.toContain("Approve");

  const result = await host.press(block, "Approve");
  expect(result.final).toEqual({ summary: "Approved by Ada" });
});
```

## References and events

| Method | What it does |
| - | - |
| `resolveReference(url)` | Matches the URL against your patterns and runs `resolve`. Returns `access`, the core fields and any rendered block. |
| `searchReferences(kind, query, limit?)` | Runs a reference kind's `search`. |
| `emit(event, data, { webhookId? })` | Delivers a lifecycle event. Sending the same `webhookId` twice reports `duplicate`, as the SDK's dedupe would in production. |

## Commands and message actions

| Method | What it does |
| - | - |
| `runCommand(name, options?, { surface? })` | Runs a [slash command](/commands) with option values, such as `{ service: "api", owner: { kind: "member", memberId, name } }`. |
| `runMessageAction(id, { text, ... }, { context?, surface? })` | Runs a [message action](/message-actions) on a message. The message defaults to one by the actor in a test conversation; pass `conversationId: null` for a chat message not filed in one. |

Both return the outcome:

| Outcome | Fields |
| - | - |
| `{ status: "answered" }` | `visibility`, and the rendered `content`, `block` and `notice` your handler answered with. |
| `{ status: "deferred" }` | `visibility`, who sees your app thinking (`"private"` unless the handler passed `{ visibility: "public" }` to `defer`), and `interactionToken`. Call `settle()`, then read the answer from the `invocations.respond` call. |

The surface defaults to a test circle. The context a message action receives follows its declared
`context`: the message's thread is the message itself, and its conversation is a test conversation
holding the message, unless you pass `context: { thread, conversation, surrounding }`. A message
with `conversationId: null`, or a `surrounding` list, gives a `"conversation"` action `surrounding`
messages instead of a conversation.

```tsx theme={null}
it("answers a deferred recap", async () => {
  const host = createTestHost(app);
  const outcome = await host.runMessageAction(
    "recap",
    { text: "Ship it" },
    { context: { conversation: { title: "Autumn launch" } } },
  );

  await host.settle();
  const answered = await host.expectPushed("invocations.respond");

  expect(outcome).toMatchObject({ status: "deferred", visibility: "private" });
  expect(answered.answer).toMatchObject({ visibility: "private" });
});
```

## Calls to Reintersect

Your handlers' calls through `reintersect.*` go to a fake API that records them:

```ts theme={null}
await host.emit("decision.published", decision);

const reply = await host.expectPushed("replies.create");
expect(reply.conversationId).toBe(decision.decision.conversationId);
```

| Method | What it does |
| - | - |
| `pushes()` | Every recorded call. |
| `pushesOf(operation)` | Calls of one kind. |
| `expectPushed(operation, match?)` | The first matching call, or a failure. |
| `settle()` | Waits for work started with `waitUntil`. |

Operations are `blocks.insert`, `blocks.update`, `blocks.replace`, `blocks.updateWhere`,
`blocks.revalidate`, `references.push`, `references.revalidate`, `notifications.send`,
`replies.create`, `streams.sendEvent`, `invocations.respond` and `invocations.update`. An `invocations.respond` call records
the `interactionToken` and the rendered `answer`, whether your code called it or a deferred task did.
A deferred task that fails records the SDK's failure answer, a private `danger` notice:
`{ visibility: "private", notice: { tone: "danger", text } }`.

## Time

The host runs on a test clock. `await host.advance("10 minutes")` moves it, which drives
`staleAfter` refreshes on `view`, signature timestamps and token expiry. `host.now()` reads it.

## Trees

| Helper | What it does |
| - | - |
| `textOf(tree)` | The plain-text projection agents and search see, from the same function Reintersect's server uses. Facts read `Label: value`, numbers are formatted (`Error rate: 0.4%`), buttons read `[Approve]`, and anything limited to an audience is left out. Pass `await host.as(viewer).visible(block)` for the text one viewer sees. |
| `findNodes(tree, type)`, `buttonsOf(tree, actions)`, `buttonOf(tree, actions, label)` | Inspect a rendered tree and its buttons and menu items. Each button has its `label`, `buttonType`, bound `action`, `confirm`, `intent` (`"destructive"` for destructive actions) and `href`. |
| `treeFor(tree, context)` | The tree a given viewer sees, with audience applied. |
| `checkTree(tree, actions?)` | The server's issues as strings, such as `<Section> can't hold <Section> here (in the second <Section>, at content[0].content[0])`. |
| `expectTree(treeOrResult, options?)` | Fails with the issues when a tree is invalid. Pass a `TestBlock`, its `tree`, an act result's `tree` or `{ tree, actions }`; trees from the host carry their action table, so bound buttons are checked too. A tree you build by hand skips the action check. |
| `TREE_LIMITS` | The limits the checks use. |

The kit runs the server's validator, so its checks cover anatomy, nesting, labels, limits and the
rest of [Limits and rules](/blocks/limits), plus action references. `reintersect check` runs the same
checks. Pass a `validator` to run your own stricter check on every render.

Failures are tagged errors you can match on, such as `ReintersectTestControlMissingError` (no visible
button with that label), `ReintersectTestTreeInvalidError` and `ReintersectTestPushMissingError`.

## Testing recipes

Recipes are ordinary blocks, so the same host tests them. Each recipe has a Promise and an Effect test you can copy from
[`packages/sdk/src/recipes/__tests__`](https://github.com/reintersect/reintersect/tree/dev/packages/sdk/src/recipes/__tests__):

| Recipe | What its test exercises |
| - | - |
| [Approval](/recipes/approval) | Approver-only controls, approving with a final summary, the confirm on reject, refusing non-approvers. |
| [Poll](/recipes/poll) | Host tallies, closing with the winner, ties and empty polls, anonymous voters, a creator-only close. |
| [RSVP](/recipes/rsvp) | Counting answers through `onChange`, organizer-only controls, a full event, closing. |
| [Checklist](/recipes/checklist) | Toggling items, the finished state and reset, unknown items, the props schema. |
| [Deployment](/recipes/deployment) | Links and a guarded cancel, pushed progress after a refresh on view, retry and cancel. |
| [Incident](/recipes/incident) | Advancing phases, posting updates from the form, resolving behind a confirm. |
| [Ticket](/recipes/ticket) | The workflow, closing behind a confirm, and use as a reference's block. |
| [Metrics panel](/recipes/metrics-panel) | The empty panel, one range, switching ranges after a push. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.