@ripplo/testing 0.0.11 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,22 @@ npm install @ripplo/testing
11
11
  ## Quick Start
12
12
 
13
13
  1. Run `npx ripplo` to authenticate and scaffold a `.ripplo/` directory in your project
14
- 2. Define preconditions in `.ripplo/preconditions/`, then add `import "./preconditions/<file>.js";` to `.ripplo/index.ts`. Exports set up test data (users, projects, etc.)
15
- 3. Write tests in `.ripplo/tests/`, then add `import "./tests/<id>.js";` to `.ripplo/index.ts`. The CLI only loads what that file imports.
16
- 4. Run `npx ripplo lint` to validate, `npx ripplo run` to execute
17
- 5. Commit the generated `.ripplo/ripplo.lock` alongside your DSL changes (see Lockfile below)
14
+ 2. Define preconditions in `.ripplo/preconditions/`, then add `import "./preconditions/<file>.js";` to `.ripplo/index.ts`. Exports set up test data (users, workspaces, etc.)
15
+ 3. (Optional) Define observers in `.ripplo/observers/` for backend state assertions (`assert.backend(...)`), then add `import "./observers/<file>.js";` to `.ripplo/index.ts`
16
+ 4. Write tests in `.ripplo/tests/`, then add `import "./tests/<id>.js";` to `.ripplo/index.ts`. The CLI only loads what that file imports.
17
+ 5. Run `npx ripplo lint` to validate, `npx ripplo run` to execute
18
+ 6. Commit the generated `.ripplo/ripplo.lock` alongside your DSL changes (see Lockfile below)
18
19
 
19
- Every test gets a clean slate via preconditions — no shared state, no ordering dependencies, fully parallelizable.
20
+ Every test gets a clean slate via preconditions — no shared state, no ordering dependencies, fully parallelizable. Observers let tests verify backend side effects (async jobs, DB rows, webhooks) without polling or sleeps in test code.
21
+
22
+ ## Architecture
23
+
24
+ `@ripplo/testing` has two halves:
25
+
26
+ - **DSL** — the typed builder (`ripplo.test(...)`, `ripplo.precondition(...)`, `ripplo.observer(...)`, locators, actions, `assert.*`) that your `.ripplo/*.ts` files use. Pure TypeScript, compiles to a JSON lockfile.
27
+ - **Engine** — the adapter (`@ripplo/testing/express`, `/fastify`, `/nextjs`) mounted into your app server. It hosts three routes under a single prefix (default `/ripplo`): `PUT /execute-preconditions`, `PUT /execute-observer`, `PUT /teardown-preconditions`. Every request is HMAC-signed; the adapter is gated behind `ENABLE_RIPPLO_TESTING` so it can't ship to production by accident.
28
+
29
+ Your precondition and observer implementations live in the server package where they have access to your DB/ORM. The DSL package never runs that code — it just orchestrates execution over signed HTTP.
20
30
 
21
31
  ## Lockfile
22
32
 
@@ -34,16 +44,29 @@ The Ripplo server reads the lockfile on every GitHub push webhook. If the lockfi
34
44
 
35
45
  ```typescript
36
46
  import ripplo from "../ripplo.js";
37
- import { dataProject } from "../preconditions/index.js";
47
+ import { dataWorkspace } from "../preconditions/index.js";
48
+ import { invitePendingForEmail } from "../observers/index.js";
49
+ import { click, fill } from "@ripplo/testing/actions";
50
+ import { assert } from "@ripplo/testing/assert";
51
+ import { role } from "@ripplo/testing/locators";
38
52
 
39
53
  ripplo
40
- .test("delete-project")
41
- .name("Delete a project")
42
- .requires({ project: dataProject })
43
- .expectedOutcome("Project deleted and user redirected to connect page")
44
- .startsAt(({ project }) => `/projects/${project.projectId}/settings`)
45
- .steps(({ project }) => [
46
- // steps here
54
+ .test("invite-a-teammate")
55
+ .name("Invite a teammate")
56
+ .requires({ workspace: dataWorkspace })
57
+ .expectedOutcome("Invite appears in the pending list and an invite record is created")
58
+ .startsAt(({ workspace }) => `/workspaces/${workspace.id}/members`)
59
+ .steps(({ workspace }) => [
60
+ click(role("button", "Invite member")).as("open invite dialog"),
61
+ fill(role("textbox", "Email"), "jamie@example.com").as("enter email"),
62
+ click(role("button", "Send invite")).as("send"),
63
+ assert.visible(role("status", "Invite sent")).as("confirm toast"),
64
+ assert
65
+ .backend(invitePendingForEmail, {
66
+ workspaceId: workspace.id,
67
+ email: "jamie@example.com",
68
+ })
69
+ .as("confirm invite recorded"),
47
70
  ]);
48
71
  ```
49
72
 
@@ -198,7 +221,7 @@ ripplo
198
221
  ### Precondition Implementation
199
222
 
200
223
  ```typescript
201
- ripplo.implement(authLoggedIn, {
224
+ ripplo.implementPrecondition(authLoggedIn, {
202
225
  setup: async (ctx, deps) => {
203
226
  const email = ctx.uniqueEmail();
204
227
  // Create user, set cookies via ctx.setCookie()
@@ -218,6 +241,92 @@ ripplo.implement(authLoggedIn, {
218
241
  - `ctx.uniqueEmail()` — generate unique email
219
242
  - `ctx.setCookie(name, value, options?)` — inject auth cookies
220
243
 
244
+ ## Observer System
245
+
246
+ Observers are backend state assertions evaluated mid-test. Use them when the UI is optimistic, the effect is async (jobs, webhooks, pubsub), or a load-bearing write needs independent verification. An observer is a named yes/no question about backend state — if a test needs to _read_ state for later reuse, that's a precondition, not an observer.
247
+
248
+ ### Declaration (`.ripplo/observers/*.ts`)
249
+
250
+ ```typescript
251
+ import type { ObserverHandle } from "@ripplo/testing";
252
+ import ripplo from "../ripplo.js";
253
+
254
+ export const orgNameIs: ObserverHandle<{ orgId: string; expectedName: string }> = ripplo
255
+ .observer("org:name-is")
256
+ .description("Org with the given id has the given name in the DB")
257
+ .input<{ orgId: string; expectedName: string }>()
258
+ .budget("fast") // optional; "fast" is default
259
+ .contract();
260
+ ```
261
+
262
+ **Chain:** `.observer(name)` → `.description(text)` → `.input<T>()` → `.budget(tier)` (optional) → `.contract()`
263
+
264
+ **Budget tiers** (framework-defined, not numeric):
265
+
266
+ - `"fast"` — ~5s with 100→1000ms backoff. Default. Synchronous DB reads.
267
+ - `"slow"` — ~30s with 250→2000ms backoff. Queue drains, replication settling.
268
+ - `"async"` — ~120s with 500→5000ms backoff. Webhooks, queue workers, LLM calls.
269
+
270
+ ### Implementation (server side)
271
+
272
+ ```typescript
273
+ ripplo.implementObserver(orgNameIs, async (ctx, { orgId, expectedName }) => {
274
+ const org = await prisma.organization.findUnique({
275
+ select: { name: true },
276
+ where: { id: orgId },
277
+ });
278
+ if (org == null) return ctx.retry(`organization "${orgId}" not found yet`);
279
+ if (org.name !== expectedName)
280
+ return ctx.retry(`name is "${org.name}", expected "${expectedName}"`);
281
+ return ctx.pass();
282
+ });
283
+ ```
284
+
285
+ **ObserverContext provides:**
286
+
287
+ - `ctx.pass()` → assertion satisfied; stop polling.
288
+ - `ctx.retry(reason)` → **default**. Use for anything that might succeed on a later poll: row not found yet, status not transitioned, queue not drained, side effect still in flight. Runtime polls until the budget expires; the last `reason` surfaces in the failure detail.
289
+ - `ctx.fail(reason)` → **narrow**. Only when further polling cannot help — an invariant has been violated (wrong shape, contradictory value, forbidden state). Stops immediately. If you're tempted to use `fail` for "not found" or "not yet X", it should be `retry`.
290
+ - Any thrown exception is treated as `fail` with the error message.
291
+
292
+ ### Usage in tests
293
+
294
+ ```typescript
295
+ import { orgNameIs } from "../observers/index.js";
296
+
297
+ ripplo
298
+ .test("update-org-name")
299
+ .requires({ project: dataProject })
300
+ .steps(({ project }) => [
301
+ fill(role("textbox", "Organization name"), "New Name").as("fill new name"),
302
+ click(role("button", "Save")).as("click save"),
303
+ assert
304
+ .backend(orgNameIs, { orgId: project.orgId, expectedName: "New Name" })
305
+ .as("assert org name in db"),
306
+ ]);
307
+ ```
308
+
309
+ ### Observer lint rules
310
+
311
+ - **`mutation-without-observer-coverage`** — flags save/create/delete/update/etc. clicks, uploads, and accepted dialogs that are not followed by an `assert.backend(...)` before the next mutation or end of test. The expected fix is always to **add an observer**.
312
+ - **`observer-params-reference-variables`** — flags observer assertions whose params are all hardcoded strings while the test declares precondition variables; use precondition data instead of literals.
313
+
314
+ ### Opting out (`uiOnly`)
315
+
316
+ For steps that genuinely don't touch server state (cancel dialogs, toggle a display-only control, pick a client-side sort option) pass `{ uiOnly: true }` to the step factory:
317
+
318
+ ```typescript
319
+ click(role("button", "Cancel"), { uiOnly: true }).as("close dialog");
320
+ ```
321
+
322
+ For entire presentation-only flows, pass the flag to `ripplo.test`:
323
+
324
+ ```typescript
325
+ ripplo.test("filter-sort-user-flows", { uiOnly: true }).name("Filter & sort");
326
+ ```
327
+
328
+ Treat `uiOnly` as a narrow escape hatch — default to writing an observer.
329
+
221
330
  ## Determinism Rules
222
331
 
223
332
  1. **Use `role()` locators exclusively.** Only use `testId()` when no ARIA role is available.
@@ -226,7 +335,7 @@ ripplo.implement(authLoggedIn, {
226
335
  4. **Every step must have `.as("description")`.** No unlabeled steps.
227
336
  5. **No duplicate labels** within a test.
228
337
  6. **End with assertions** that verify the `expectedOutcome`.
229
- 7. **After a test passes, run flake detection** to verify determinism across parallel runs.
338
+ 7. **Cover backend mutations with observers.** Any save/create/delete/update click (or upload, or accepted dialog) must be verified with `assert.backend(observerHandle, params)` unless it truly has no server effect — in which case mark the step `{ uiOnly: true }`.
230
339
 
231
340
  ## CLI Commands
232
341
 
@@ -239,7 +348,7 @@ ripplo flake-detect <id> --runs=10 # Run N times in parallel to detect flakines
239
348
 
240
349
  ## Server Setup
241
350
 
242
- Your application server must expose the precondition endpoints under a single path prefix (the value you pass to `createRipplo({ preconditionsUrl })`). Pick the adapter that matches your framework — each handles webhook signature verification, cookie forwarding, and request parsing for you. Every adapter takes a required `enabled: boolean` flag — bind it to an env var (e.g. `process.env.ENABLE_RIPPLO_TESTING === "true"`) so the endpoints never ship to production. When `enabled` is false the adapter mounts a no-op handler.
351
+ Your application server must expose the engine endpoint under a single path prefix (the value you pass to `createRipplo({ engineUrl })`). Pick the adapter that matches your framework — each handles webhook signature verification, cookie forwarding, and request parsing for you. Every adapter takes a required `enabled: boolean` flag — bind it to an env var (e.g. `process.env.ENABLE_RIPPLO_TESTING === "true"`) so the endpoints never ship to production. When `enabled` is false the adapter mounts a no-op handler.
243
352
 
244
353
  ### Express
245
354
 
@@ -250,12 +359,12 @@ import ripplo from "<path to .ripplo/ripplo>"; // import the existing instance
250
359
 
251
360
  const app = express();
252
361
  app.use(
253
- "/ripplo/preconditions",
362
+ "/ripplo",
254
363
  createExpressHandler({ enabled: process.env.ENABLE_RIPPLO_TESTING === "true", ripplo }),
255
364
  );
256
365
  ```
257
366
 
258
- Mounts both `PUT /execute-batch` and `PUT /teardown` under the prefix you choose.
367
+ Mounts both `PUT /execute-preconditions` and `PUT /teardown-preconditions` under the prefix you choose.
259
368
 
260
369
  ### Fastify
261
370
 
@@ -267,7 +376,7 @@ import ripplo from "<path to .ripplo/ripplo>"; // import the existing instance
267
376
  const app = Fastify();
268
377
  await app.register(
269
378
  registerFastifyHandler({ enabled: process.env.ENABLE_RIPPLO_TESTING === "true", ripplo }),
270
- { prefix: "/ripplo/preconditions" },
379
+ { prefix: "/ripplo" },
271
380
  );
272
381
  ```
273
382
 
@@ -276,7 +385,7 @@ await app.register(
276
385
  The Next.js adapter exports a single catch-all handler. Create one dynamic route file:
277
386
 
278
387
  ```ts
279
- // app/ripplo/preconditions/[action]/route.ts
388
+ // app/ripplo/[action]/route.ts
280
389
  import { createNextHandler } from "@ripplo/testing/nextjs";
281
390
  import ripplo from "@/.ripplo/ripplo";
282
391
 
@@ -286,7 +395,7 @@ export const PUT = createNextHandler({
286
395
  });
287
396
  ```
288
397
 
289
- The handler dispatches on the last URL segment (`execute-batch` or `teardown`) and returns 404 for anything else. It depends only on the Web `Request` / `Response` types, so it runs on both the Node and Edge runtimes — no `next` import required.
398
+ The handler dispatches on the last URL segment (`execute-preconditions` or `teardown-preconditions`) and returns 404 for anything else. It depends only on the Web `Request` / `Response` types, so it runs on both the Node and Edge runtimes — no `next` import required.
290
399
 
291
400
  ### Custom integration (raw engine)
292
401
 
@@ -304,7 +413,7 @@ import ripplo from "<path to .ripplo/ripplo>"; // import the existing instance
304
413
  const engine = createEngine(ripplo);
305
414
  const webhookSecret = ripplo.getConfig().webhookSecret;
306
415
 
307
- // PUT /ripplo/preconditions/execute-batch
416
+ // PUT /ripplo/execute-preconditions
308
417
  async function executeBatch(req: Request): Promise<Response> {
309
418
  const body = await req.text();
310
419
  const verified = verifyWebhookSignature(
@@ -331,7 +440,7 @@ async function executeBatch(req: Request): Promise<Response> {
331
440
  return new Response(JSON.stringify(result), { headers });
332
441
  }
333
442
 
334
- // PUT /ripplo/preconditions/teardown
443
+ // PUT /ripplo/teardown
335
444
  async function teardown(req: Request): Promise<Response> {
336
445
  // ... same verify pattern, then:
337
446
  // await engine.teardown(parsed.preconditions, parsed.data);
@@ -341,7 +450,7 @@ async function teardown(req: Request): Promise<Response> {
341
450
  **You're responsible for:**
342
451
 
343
452
  - **Webhook verification.** Always call `verifyWebhookSignature` before invoking the engine.
344
- - **Routing.** Dispatch the two endpoints (`execute-batch`, `teardown`) however your framework handles routes.
453
+ - **Routing.** Dispatch the two endpoints (`execute-preconditions`, `teardown-preconditions`) however your framework handles routes.
345
454
  - **Cookie forwarding.** `result.cookies` contains the cookies preconditions set during setup — they must reach the test browser as `Set-Cookie` headers, or login/session preconditions will silently fail.
346
455
  - **Body parsing.** Use the raw text body for signature verification, then `JSON.parse` for the engine call.
347
456
 
@@ -355,7 +464,7 @@ import { createRipplo } from "@ripplo/testing";
355
464
 
356
465
  export default createRipplo({
357
466
  appUrl: process.env.APP_URL,
358
- preconditionsUrl: `${process.env.APP_URL}/ripplo/preconditions`,
467
+ engineUrl: `${process.env.APP_URL}/ripplo`,
359
468
  projectId: "...",
360
469
  webhookSecret: process.env.RIPPLO_WEBHOOK_SECRET,
361
470
  });
@@ -365,7 +474,7 @@ export default createRipplo({
365
474
 
366
475
  The app server exposes two endpoints for test data management:
367
476
 
368
- ### Execute (`PUT {preconditionApiPath}/execute`)
477
+ ### Execute (`PUT {engineBaseUrl}/execute`)
369
478
 
370
479
  ```json
371
480
  // Request
@@ -377,7 +486,7 @@ The app server exposes two endpoints for test data management:
377
486
 
378
487
  Returns data that flows into test variables. Set-Cookie headers are captured automatically.
379
488
 
380
- ### Teardown (`PUT {preconditionApiPath}/teardown`)
489
+ ### Teardown (`PUT {engineBaseUrl}/teardown`)
381
490
 
382
491
  ```json
383
492
  // Request
package/dist/actions.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { U as UnlabeledStep } from './step-DLfkKI3V.js';
1
+ import { U as UnlabeledStep } from './step-De52hTLd.js';
2
2
  import { CheckLocator, InputLocator, AnyLocator, SelectLocator } from './locators.js';
3
3
  import '@ripplo/spec';
4
4
 
@@ -9,7 +9,10 @@ declare function navigate(url: string): UnlabeledStep<{
9
9
  value: string;
10
10
  };
11
11
  }>;
12
- declare function click(locator: AnyLocator): UnlabeledStep<{
12
+ interface StepOptions {
13
+ readonly uiOnly?: boolean;
14
+ }
15
+ declare function click(locator: AnyLocator, options?: StepOptions): UnlabeledStep<{
13
16
  locator: {
14
17
  by: "testId";
15
18
  value: string;
@@ -19,6 +22,7 @@ declare function click(locator: AnyLocator): UnlabeledStep<{
19
22
  name?: string | undefined;
20
23
  };
21
24
  type: "click";
25
+ uiOnly: boolean | undefined;
22
26
  }>;
23
27
  declare function fill(locator: InputLocator, value: string): UnlabeledStep<{
24
28
  locator: {
@@ -87,7 +91,7 @@ declare function press(key: string): UnlabeledStep<{
87
91
  key: string;
88
92
  type: "press";
89
93
  }>;
90
- declare function upload(locator: AnyLocator, path: string): UnlabeledStep<{
94
+ declare function upload(locator: AnyLocator, path: string, options?: StepOptions): UnlabeledStep<{
91
95
  files: string[];
92
96
  locator: {
93
97
  by: "testId";
@@ -98,6 +102,7 @@ declare function upload(locator: AnyLocator, path: string): UnlabeledStep<{
98
102
  name?: string | undefined;
99
103
  };
100
104
  type: "upload";
105
+ uiOnly: boolean | undefined;
101
106
  }>;
102
107
  declare function dblclick(locator: AnyLocator): UnlabeledStep<{
103
108
  locator: {
@@ -191,11 +196,13 @@ declare function drag(source: AnyLocator, target: AnyLocator): UnlabeledStep<{
191
196
  interface HandleDialogOptions {
192
197
  readonly action: "accept" | "dismiss";
193
198
  readonly promptText: string | undefined;
199
+ readonly uiOnly?: boolean;
194
200
  }
195
- declare function handleDialog({ action, promptText }: HandleDialogOptions): UnlabeledStep<{
201
+ declare function handleDialog({ action, promptText, uiOnly }: HandleDialogOptions): UnlabeledStep<{
196
202
  action: "accept" | "dismiss";
197
203
  promptText: string | undefined;
198
204
  type: "handleDialog";
205
+ uiOnly: boolean | undefined;
199
206
  }>;
200
207
  interface ClipboardOptions {
201
208
  readonly action: "read" | "write";
package/dist/actions.js CHANGED
@@ -10,8 +10,12 @@ import "./chunk-DCJBLS2U.js";
10
10
  function navigate(url) {
11
11
  return createStep({ type: "goto", url: { type: "static", value: url } });
12
12
  }
13
- function click(locator) {
14
- return createStep({ locator: toSpecLocator(locator), type: "click" });
13
+ function click(locator, options) {
14
+ return createStep({
15
+ locator: toSpecLocator(locator),
16
+ type: "click",
17
+ uiOnly: options?.uiOnly
18
+ });
15
19
  }
16
20
  function fill(locator, value) {
17
21
  return createStep({
@@ -39,11 +43,12 @@ function hover(locator) {
39
43
  function press(key) {
40
44
  return createStep({ key, type: "press" });
41
45
  }
42
- function upload(locator, path) {
46
+ function upload(locator, path, options) {
43
47
  return createStep({
44
48
  files: [path],
45
49
  locator: toSpecLocator(locator),
46
- type: "upload"
50
+ type: "upload",
51
+ uiOnly: options?.uiOnly
47
52
  });
48
53
  }
49
54
  function dblclick(locator) {
@@ -75,8 +80,8 @@ function drag(source, target) {
75
80
  type: "drag"
76
81
  });
77
82
  }
78
- function handleDialog({ action, promptText }) {
79
- return createStep({ action, promptText, type: "handleDialog" });
83
+ function handleDialog({ action, promptText, uiOnly }) {
84
+ return createStep({ action, promptText, type: "handleDialog", uiOnly });
80
85
  }
81
86
  function clipboard({ action, value, variable }) {
82
87
  return createStep({
package/dist/assert.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { U as UnlabeledStep } from './step-DLfkKI3V.js';
1
+ import { O as ObserverHandle, a as ObserverInput, b as ObserverBudgetTier } from './types-oYS_Yv4G.js';
2
+ import { U as UnlabeledStep } from './step-De52hTLd.js';
2
3
  import { CheckLocator, AnyLocator } from './locators.js';
4
+ import 'zod';
3
5
  import '@ripplo/spec';
4
6
 
5
7
  declare const assert: {
@@ -66,6 +68,15 @@ declare const assert: {
66
68
  operator: "equals";
67
69
  type: "assertAttribute";
68
70
  }>;
71
+ backend<THandle extends ObserverHandle>(observer: THandle, params: ObserverInput<THandle> & Record<string, string>): UnlabeledStep<{
72
+ budget: ObserverBudgetTier;
73
+ observer: string;
74
+ params: Record<string, {
75
+ readonly type: "static";
76
+ readonly value: string;
77
+ }>;
78
+ type: "assertObserver";
79
+ }>;
69
80
  checked(locator: CheckLocator): UnlabeledStep<{
70
81
  locator: {
71
82
  by: "testId";
package/dist/assert.js CHANGED
@@ -4,6 +4,10 @@ import {
4
4
  import {
5
5
  createStep
6
6
  } from "./chunk-MGATMMCZ.js";
7
+ import {
8
+ readObserverBudget,
9
+ readObserverName
10
+ } from "./chunk-3IL457A7.js";
7
11
  import "./chunk-DCJBLS2U.js";
8
12
 
9
13
  // src/steps/assert.ts
@@ -31,6 +35,14 @@ var assert = {
31
35
  type: "assertAttribute"
32
36
  });
33
37
  },
38
+ backend(observer, params) {
39
+ return createStep({
40
+ budget: readObserverBudget(observer),
41
+ observer: readObserverName(observer),
42
+ params: paramsToStringRefs(params),
43
+ type: "assertObserver"
44
+ });
45
+ },
34
46
  checked(locator) {
35
47
  return createStep({ locator: toSpecLocator(locator), type: "assertChecked" });
36
48
  },
@@ -78,6 +90,13 @@ var assert = {
78
90
  return createStep({ locator: toSpecLocator(locator), type: "assertVisible" });
79
91
  }
80
92
  };
93
+ function paramsToStringRefs(params) {
94
+ const out = {};
95
+ Object.entries(params).forEach(([key, value]) => {
96
+ out[key] = { type: "static", value };
97
+ });
98
+ return out;
99
+ }
81
100
  export {
82
101
  assert
83
102
  };
@@ -0,0 +1,80 @@
1
+ import { O as ObserverHandle, P as Precondition, j as PreconditionData, k as TestValue, S as SetupContext, T as TeardownContext, f as DslConfig, h as ObserverDefinition, l as PreconditionDefinition, m as TestDefinition, U as UnimplementedItems, g as ObserverContext, c as ObserverOutcome, a as ObserverInput } from './types-oYS_Yv4G.js';
2
+ import { ObserverBudget } from '@ripplo/spec';
3
+ import { S as Step } from './step-De52hTLd.js';
4
+
5
+ interface ObserverNeedsInput {
6
+ readonly budget: (tier: ObserverBudget) => ObserverNeedsInput;
7
+ readonly description: (text: string) => ObserverNeedsInput;
8
+ readonly input: <TInput extends Record<string, string>>() => ObserverReady<TInput>;
9
+ }
10
+ interface ObserverReady<TInput extends Record<string, string>> {
11
+ readonly contract: () => ObserverHandle<TInput>;
12
+ readonly notImplemented: () => ObserverHandle<TInput>;
13
+ }
14
+
15
+ type PreconditionRecord = Record<string, Precondition>;
16
+ type ResolveDeps<TDeps extends PreconditionRecord> = {
17
+ readonly [K in keyof TDeps]: PreconditionData<TDeps[K]>;
18
+ };
19
+ type ResolveData<T extends Record<string, TestValue>> = {
20
+ readonly [K in keyof T]: string;
21
+ };
22
+ interface PreconditionNeedsSetup {
23
+ readonly contract: <TData extends Record<string, string>>() => Precondition<TData>;
24
+ readonly description: (text: string) => PreconditionNeedsSetup;
25
+ readonly notImplemented: () => Precondition<Record<string, never>>;
26
+ readonly requires: <TDeps extends PreconditionRecord>(deps: TDeps) => PreconditionNeedsSetupWithDeps<TDeps>;
27
+ readonly setup: <TData extends Record<string, TestValue>>(fn: (ctx: SetupContext) => Promise<TData>) => PreconditionHasSetup<ResolveData<TData>>;
28
+ }
29
+ interface PreconditionNeedsSetupWithDeps<TDeps extends PreconditionRecord> {
30
+ readonly contract: <TData extends Record<string, string>>() => Precondition<TData, ResolveDeps<TDeps>>;
31
+ readonly description: (text: string) => PreconditionNeedsSetupWithDeps<TDeps>;
32
+ readonly notImplemented: () => Precondition<Record<string, never>>;
33
+ readonly requires: <TDeps2 extends PreconditionRecord>(deps: TDeps2) => PreconditionNeedsSetupWithDeps<TDeps2>;
34
+ readonly setup: <TData extends Record<string, TestValue>>(fn: (ctx: SetupContext, deps: ResolveDeps<TDeps>) => Promise<TData>) => PreconditionHasSetup<ResolveData<TData>>;
35
+ }
36
+ interface PreconditionHasSetup<TData extends Record<string, string>> {
37
+ readonly teardown: (fn: (ctx: TeardownContext<TData>) => Promise<void>) => Precondition<TData>;
38
+ }
39
+ interface TestNeedsName {
40
+ readonly name: (displayName: string) => TestNeedsRequires;
41
+ }
42
+ interface TestNeedsRequires {
43
+ readonly description: (text: string) => TestNeedsRequires;
44
+ readonly requires: <TReqs extends PreconditionRecord>(reqs: TReqs) => TestNeedsOutcome<ResolveDeps<TReqs>>;
45
+ }
46
+ interface TestNeedsOutcome<TVars extends Record<string, Record<string, string>>> {
47
+ readonly description: (text: string) => TestNeedsOutcome<TVars>;
48
+ readonly expectedOutcome: (text: string) => TestNeedsStartsAt<TVars>;
49
+ }
50
+ interface TestNeedsStartsAt<TVars extends Record<string, Record<string, string>>> {
51
+ readonly notImplemented: () => void;
52
+ readonly startsAt: (fn: (vars: TVars) => string) => TestNeedsSteps<TVars>;
53
+ }
54
+ interface TestNeedsSteps<TVars extends Record<string, Record<string, string>>> {
55
+ readonly steps: (fn: (vars: TVars) => ReadonlyArray<Step>) => void;
56
+ }
57
+
58
+ interface PreconditionImpl<TData extends Record<string, string>, TDeps extends Record<string, Record<string, string>>> {
59
+ readonly setup: (ctx: SetupContext, deps: TDeps) => Promise<Record<string, TestValue>>;
60
+ readonly teardown: (ctx: TeardownContext<TData>) => Promise<void>;
61
+ }
62
+ type ObserverImplFn<TInput extends Record<string, string>> = (ctx: ObserverContext, params: TInput) => Promise<ObserverOutcome>;
63
+ interface RipploBuilder {
64
+ readonly getConfig: () => DslConfig;
65
+ readonly getObservers: () => ReadonlyArray<ObserverDefinition>;
66
+ readonly getPreconditions: () => ReadonlyArray<PreconditionDefinition>;
67
+ readonly getTests: () => ReadonlyArray<TestDefinition>;
68
+ readonly getUnimplemented: () => UnimplementedItems;
69
+ readonly implementObserver: <THandle extends ObserverHandle>(handle: THandle, impl: ObserverImplFn<ObserverInput<THandle> & Record<string, string>>) => void;
70
+ readonly implementPrecondition: <TData extends Record<string, string>, TDeps extends Record<string, Record<string, string>>>(handle: Precondition<TData, TDeps>, impl: PreconditionImpl<TData, TDeps>) => void;
71
+ readonly observer: (name: string) => ObserverNeedsInput;
72
+ readonly precondition: (name: string) => PreconditionNeedsSetup;
73
+ readonly test: (id: string, options?: TestOptions) => TestNeedsName;
74
+ }
75
+ interface TestOptions {
76
+ readonly uiOnly?: boolean;
77
+ }
78
+ declare function createRipplo(rawConfig: DslConfig): RipploBuilder;
79
+
80
+ export { type ObserverImplFn as O, type PreconditionImpl as P, type RipploBuilder as R, type PreconditionRecord as a, type ResolveDeps as b, createRipplo as c };
@@ -0,0 +1,60 @@
1
+ // src/types.ts
2
+ import { z } from "zod";
3
+ var DEFAULT_WATCH_PATHS = [
4
+ "src/**",
5
+ "app/**",
6
+ "apps/**",
7
+ "pages/**",
8
+ "routes/**",
9
+ "components/**"
10
+ ];
11
+ var DEFAULT_IGNORE_PATHS = [
12
+ "**/*.gen.*",
13
+ "**/generated/**",
14
+ "**/*.d.ts",
15
+ "**/*.test.*",
16
+ "**/*.spec.*",
17
+ "**/node_modules/**",
18
+ "**/dist/**",
19
+ "**/build/**",
20
+ ".ripplo/**",
21
+ "**/*.md"
22
+ ];
23
+ var dslConfigSchema = z.object({
24
+ appUrl: z.string(),
25
+ engineUrl: z.string(),
26
+ ignorePaths: z.array(z.string()).optional(),
27
+ projectId: z.string(),
28
+ watchPaths: z.array(z.string()).optional(),
29
+ webhookSecret: z.string()
30
+ });
31
+ function readTestValue(value) {
32
+ return value.value;
33
+ }
34
+ function createTestValue(value) {
35
+ return { value };
36
+ }
37
+ function readPreconditionName(p) {
38
+ return p.name;
39
+ }
40
+ function readObserverName(o) {
41
+ return o.name;
42
+ }
43
+ function readObserverBudget(o) {
44
+ return o.budget;
45
+ }
46
+ function makeObserverHandle(name, budget) {
47
+ return { budget, name };
48
+ }
49
+
50
+ export {
51
+ DEFAULT_WATCH_PATHS,
52
+ DEFAULT_IGNORE_PATHS,
53
+ dslConfigSchema,
54
+ readTestValue,
55
+ createTestValue,
56
+ readPreconditionName,
57
+ readObserverName,
58
+ readObserverBudget,
59
+ makeObserverHandle
60
+ };