@ripplo/testing
v0.8.0
Published
Typed test DSL for Ripplo — declare entities and user flows, compile to a lockfile
Readme
@ripplo/testing
Typed test DSL for Ripplo. You declare your app's state model and user flows in TypeScript; Ripplo compiles them to a lockfile and executes them against your real app with real backend state.
This package is the authoring surface. Execution lives in the ripplo CLI.
Install
npx ripplo setup (guided, via the Claude Code plugin) or npx ripplo init (by hand) scaffolds .ripplo/, installs this package, and wires env vars.
The model
A project has four layers, all plain TypeScript under .ripplo/:
- Entities — the state model. Each entity declares its fields as value-spaces (
v.email(),v.number({ min, max }), ...) and gets aseed/readimplementation in your app's engine. - Singletons — client/global state (e.g. localStorage flags).
- Worlds — pure functions returning a record of entity handles: the starting state a test runs against.
- Tests —
test("Intent", () => ({ given, steps })).givenarranges;stepsact and assert.
import { button, click, fill, goto, test, textbox, visible } from "@ripplo/testing";
import { Task } from "../entities/index.js";
import { ownedProject } from "../worlds/index.js";
export const createTask = test("Create a task", () => {
const { me, project } = ownedProject();
return {
given: [me, project],
steps: [
goto`/projects/${project.id}/tasks`.expect(visible(button("New"))),
click(button("New")).expect(visible(textbox("Title"))),
fill(textbox("Title"), "Buy milk"),
click(button("Create")).expect(Task.created({ title: "Buy milk", projectId: project.id })),
],
};
});arbitrary(Entity.field.x) draws a fresh value per run from the field's value-space, so tests exercise the space instead of one hardcoded value. Backend effects (created, updated, deleted) are checked by an oracle that compares model-before, predicted-after, and observed state.
The complete primitive catalog — every action, locator, predicate, field axis, and assertion — is in DSL.md, shipped with the package.
The engine adapter
Your app exposes a test engine: seed/read implementations for each declared entity, mounted behind a signed-webhook endpoint that runs only when explicitly enabled.
// src/test/engine.ts
import { createEngine } from "@ripplo/testing";
import ripplo from "../../.ripplo/index.js";
import { impls } from "./impls.js";
export const engine = createEngine(ripplo, { entities: impls, singletons: {} }, teardown);Mount it on whatever server you run. Every adapter exposes the same three signed PUT routes — /setup, /state, /teardown — under the prefix you mount it at. Point RIPPLO_ENGINE_URL at that prefix.
// Express
import { createEngineHandler } from "@ripplo/testing/express";
app.use("/ripplo", createEngineHandler({ enabled, engine }));
// Fastify
import { registerFastifyHandler } from "@ripplo/testing/fastify";
app.register(registerFastifyHandler({ enabled, engine }), { prefix: "/ripplo" });
// Hono
import { createHonoHandler } from "@ripplo/testing/hono";
app.route("/ripplo", createHonoHandler({ enabled, engine }));
// Koa
import { createKoaHandler } from "@ripplo/testing/koa";
app.use(createKoaHandler({ enabled, engine }));
// Elysia
import { createElysiaHandler } from "@ripplo/testing/elysia";
app.use(createElysiaHandler({ enabled, engine }));
// Next.js — app/ripplo/[action]/route.ts
import { createNextHandler } from "@ripplo/testing/nextjs";
export const PUT = createNextHandler({ enabled, engine });
// NestJS — functional middleware
import { createNestHandler } from "@ripplo/testing/nestjs";
consumer.apply(createNestHandler({ enabled, engine })).forRoutes("ripplo");enabled gates the adapter — leave it off in production. TypeScript enforces one implementation per declared entity; a missing or duplicate impl is a compile error.
Compile, commit, run
npx ripplo compile # analyze + typecheck .ripplo/ → ripplo.lock
npx ripplo run # execute against your local app via the daemonCommit .ripplo/ripplo.lock alongside your test changes. The Ripplo server reads it verbatim on every push.
License
© Ripplo LLC. All rights reserved. Use is subject to Ripplo's Terms of Service.
