@ripplo/testing 0.0.11 → 0.1.0
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 +136 -27
- package/dist/actions.d.ts +11 -4
- package/dist/actions.js +11 -6
- package/dist/assert.d.ts +12 -1
- package/dist/assert.js +19 -0
- package/dist/builder-c7tXey03.d.ts +80 -0
- package/dist/chunk-3IL457A7.js +60 -0
- package/dist/{chunk-7ETQVVAA.js → chunk-CD3M7H5A.js} +127 -46
- package/dist/{chunk-LEIKZ6BE.js → chunk-KNF4K4JH.js} +12 -4
- package/dist/compiler.d.ts +5 -3
- package/dist/compiler.js +1 -1
- package/dist/control.d.ts +1 -1
- package/dist/express.d.ts +3 -2
- package/dist/express.js +19 -3
- package/dist/fastify.d.ts +3 -2
- package/dist/fastify.js +17 -3
- package/dist/index.d.ts +13 -5
- package/dist/index.js +158 -44
- package/dist/lockfile.d.ts +68 -76
- package/dist/lockfile.js +33 -13
- package/dist/nextjs.d.ts +3 -2
- package/dist/nextjs.js +29 -5
- package/dist/{step-DLfkKI3V.d.ts → step-De52hTLd.d.ts} +1 -1
- package/dist/types-oYS_Yv4G.d.ts +115 -0
- package/package.json +3 -3
- package/dist/builder-1kySbit_.d.ts +0 -137
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,
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
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 {
|
|
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("
|
|
41
|
-
.name("
|
|
42
|
-
.requires({
|
|
43
|
-
.expectedOutcome("
|
|
44
|
-
.startsAt(({
|
|
45
|
-
.steps(({
|
|
46
|
-
|
|
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.
|
|
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"` — ~3s 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)` → transient; runtime keeps polling until budget expires. `reason` surfaces in failure detail.
|
|
289
|
+
- `ctx.fail(reason)` → invariant violated; stop immediately.
|
|
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. **
|
|
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
|
|
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
|
|
362
|
+
"/ripplo",
|
|
254
363
|
createExpressHandler({ enabled: process.env.ENABLE_RIPPLO_TESTING === "true", ripplo }),
|
|
255
364
|
);
|
|
256
365
|
```
|
|
257
366
|
|
|
258
|
-
Mounts both `PUT /execute-
|
|
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
|
|
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/
|
|
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-
|
|
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/
|
|
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/
|
|
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-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
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({
|
|
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 {
|
|
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
|
+
};
|