> ## 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 an Effect app against a fake Reintersect host with @effect/vitest: makeTestHost, ReintersectTestHost.layer, fake services, TestClock and typed test failures.

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

`@reintersect/sdk/effect/testing` runs your app against an in-memory Reintersect host, the same host
as the [Promise testing kit](/testing), as an Effect. Deliveries go through your app's real router
inside the test fiber, so your services, `TestClock` and typed failures work as in any
`@effect/vitest` test.

## Set up a host

`makeTestHost(app, options)` returns an Effect that builds the host. Run it inside `it.scoped`:

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

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

describe("task block", () => {
  it.scoped("starts a task and shows its project", () =>
    Effect.gen(function* () {
      const host = yield* testHost;

      const block = yield* host.insert(TaskBlock, {
        id: "TASK-2",
        title: "Review the pricing page",
      });
      const result = yield* host.press(block, "Start");

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

  it.scoped("resolves a task link into a task block", () =>
    Effect.gen(function* () {
      const host = yield* testHost;

      const resolved = yield* host.resolveReference("https://tasks.example.com/tasks/TASK-1");

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

This is the test the Effect template writes. The [host options](/testing#host-options) are the same
as on the Promise kit, except `now`: the test clock starts where `TestClock` is.

To share one host across a suite, provide it as a service with `ReintersectTestHost.layer`:

```ts src/checklist.test.ts theme={null}
import { describe, expect, it } from "@effect/vitest";
import { createReintersectApp } from "@reintersect/sdk/effect/app";
import { checklist } from "@reintersect/sdk/effect/recipes";
import { ReintersectTestHost } from "@reintersect/sdk/effect/testing";
import { Effect } from "effect";

const LaunchChecklist = checklist({ type: "launch-checklist" });
const app = createReintersectApp({ name: "Launches" }).addBlock(LaunchChecklist);

describe("launch checklist", () => {
  it.effect("checks items off", () =>
    Effect.gen(function* () {
      const host = yield* ReintersectTestHost;

      const block = yield* host.insert(LaunchChecklist, {
        title: "Launch day",
        items: [{ id: "docs", label: "Docs" }],
      });
      const result = yield* host.press(block, "Mark Docs done");

      expect(result.state).toEqual({ done: ["docs"] });
    }).pipe(Effect.provide(ReintersectTestHost.layer(app))),
  );
});
```

## The host

The Effect host has the same methods as the [Promise host](/testing#blocks), and each returns an
`Effect`: `render`, `insert`, `act`, `press`, `vote`, `tallies`, `voters`, `view`, `visible`,
`block`, `reopen`, `search`, `resolveReference`, `searchReferences`, `runCommand`,
`runMessageAction`, `emit`, `pushesOf`, `expectPushed` and `expectTree`. `host.as(viewer)` returns a
host acting as another member.

A few differ in shape:

| Promise host | Effect host |
| - | - |
| `await host.settle()` | `yield* host.settle` |
| `await host.pushes()` | `yield* host.pushes` |
| `await host.advance("10 minutes")`, `host.now()` | `yield* TestClock.adjust("10 minutes")`, `Clock` |
| `await host.dispose()` | The test's scope closes the host. |
| Rejects the promise with a tagged error | Fails with the tagged error in the error channel |

## Fake a service

Build the host from the app before `provide`, and give the test its own layer. Here `TaskBlock`'s
`move` action is the one from [Errors](/effect/errors#turn-a-failure-into-an-answer), which calls a
`Tracker` service. The test swaps in a `Tracker` that always reports a locked task:

```ts src/moveTask.test.ts theme={null}
import { describe, expect, it } from "@effect/vitest";
import { makeTestHost } from "@reintersect/sdk/effect/testing";
import { Effect, Layer } from "effect";
import { tasksApp } from "./app";
import { TaskBlock } from "./taskBlock";
import { TaskLocked, Tracker } from "./tracker";

const LockedTracker = Layer.succeed(Tracker, {
  move: (id) => Effect.fail(new TaskLocked({ id })),
});

describe("moving a task", () => {
  it.scoped("warns when the tracker locks the task", () =>
    Effect.gen(function* () {
      const host = yield* makeTestHost(tasksApp);

      const block = yield* host.insert(TaskBlock, { id: "TASK-2", title: "Review the pricing page" });
      const result = yield* host.press(block, "Start");

      expect(result.notice).toEqual({ tone: "warning", text: "That task is locked" });
      expect(result.block.state).toEqual({ status: "todo" });
    }).pipe(Effect.provide(LockedTracker)),
  );
});
```

`makeTestHost(tasksApp)` and `ReintersectTestHost.layer(tasksApp)` require whatever services the app
hasn't been given, so the test doesn't compile until it provides them.

## Time

The host reads the time from `Clock`, so `TestClock` drives everything that depends on it, such as
`staleAfter` refreshes on `view`, signature timestamps and token expiry:

```ts theme={null}
const block = yield* host.insert(Deploy, { service: "api" });

yield* TestClock.adjust("10 minutes");

const refreshed = yield* host.view(block);
```

## Typed failures

Every host method fails with a tagged error, so you can assert on a failure as a value:

```ts theme={null}
const error = yield* Effect.flip(host.press(block, "Archive"));

expect(error._tag).toBe("ReintersectTestControlMissingError");
```

| Error | When |
| - | - |
| `ReintersectTestDeliveryError` | Your app answered a delivery with an error. It has the `status`, `code` and `issues`. |
| `ReintersectTestTreeInvalidError` | A rendered tree failed the validator Reintersect's server runs. Its issues use the messages in [Limits](/blocks/limits). |
| `ReintersectTestBlockMissingError` | The block isn't in the host. |
| `ReintersectTestBlockFinalError` | The block is final, so it takes no more actions. |
| `ReintersectTestControlMissingError` | No visible button or menu item has that label. |
| `ReintersectTestInputInvalidError` | The vote names an input or value the block doesn't have. |
| `ReintersectTestReferenceUnmatchedError` | No reference pattern of the app matches the URL. |
| `ReintersectTestPushMissingError` | `expectPushed` found no matching call. |
| `ReintersectTestReopenRefusedError` | The block isn't reopenable, or the member acting isn't its creator or an admin. |

## Trees

`textOf`, `findNodes`, `buttonsOf`, `buttonOf`, `treeFor`, `checkTree` and `TREE_LIMITS` work as on
the [Promise kit](/testing#trees). `expectTree(treeOrResult, options?)` returns an `Effect` that
fails with `ReintersectTestTreeInvalidError` when the tree is invalid.


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