@ripplo/testing 0.6.1 → 0.7.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.
Files changed (44) hide show
  1. package/DSL.md +357 -0
  2. package/LICENSE.md +1 -0
  3. package/README.md +47 -273
  4. package/dist/engine-BfvzXgLg.d.ts +1091 -0
  5. package/dist/express.d.ts +7 -9
  6. package/dist/express.js +422 -48
  7. package/dist/index.d.ts +134 -59
  8. package/dist/index.js +1654 -1126
  9. package/package.json +31 -113
  10. package/dist/actions.d.ts +0 -260
  11. package/dist/actions.js +0 -177
  12. package/dist/assert.d.ts +0 -188
  13. package/dist/assert.js +0 -111
  14. package/dist/builder-SsgqYqSC.d.ts +0 -156
  15. package/dist/chunk-2YLI7VD4.js +0 -65
  16. package/dist/chunk-4MGIQFAJ.js +0 -16
  17. package/dist/chunk-DCJBLS2U.js +0 -26
  18. package/dist/chunk-MGATMMCZ.js +0 -16
  19. package/dist/chunk-XO36IU66.js +0 -88
  20. package/dist/chunk-YFOTJIVF.js +0 -134
  21. package/dist/chunk-YQAEOH5W.js +0 -111
  22. package/dist/compiler.d.ts +0 -32
  23. package/dist/compiler.js +0 -8
  24. package/dist/control.d.ts +0 -45
  25. package/dist/control.js +0 -17
  26. package/dist/elysia.d.ts +0 -78
  27. package/dist/elysia.js +0 -114
  28. package/dist/engine-BOqzK_go.d.ts +0 -61
  29. package/dist/fastify.d.ts +0 -14
  30. package/dist/fastify.js +0 -79
  31. package/dist/hono.d.ts +0 -19
  32. package/dist/hono.js +0 -89
  33. package/dist/koa.d.ts +0 -14
  34. package/dist/koa.js +0 -135
  35. package/dist/locators.d.ts +0 -40
  36. package/dist/locators.js +0 -11
  37. package/dist/lockfile.d.ts +0 -722
  38. package/dist/lockfile.js +0 -707
  39. package/dist/nestjs.d.ts +0 -17
  40. package/dist/nestjs.js +0 -139
  41. package/dist/nextjs.d.ts +0 -14
  42. package/dist/nextjs.js +0 -137
  43. package/dist/step-De52hTLd.d.ts +0 -19
  44. package/dist/types-BzZrl65Z.d.ts +0 -115
package/dist/index.js CHANGED
@@ -1,1279 +1,1807 @@
1
- import {
2
- DEFAULT_IGNORE_PATHS,
3
- DEFAULT_WATCH_PATHS,
4
- brandTestValue,
5
- isPrimitive,
6
- makeObserverHandle,
7
- readObserverBudget,
8
- readObserverDescription,
9
- readObserverName,
10
- readPreconditionDepMapping,
11
- readPreconditionDependsOn,
12
- readPreconditionDescription,
13
- readPreconditionName
14
- } from "./chunk-YQAEOH5W.js";
15
- import {
16
- compile
17
- } from "./chunk-YFOTJIVF.js";
18
- import "./chunk-MGATMMCZ.js";
19
- import {
20
- readAdapterWebhookSecret,
21
- serializeCookie,
22
- toBatchRunResults,
23
- toTeardownResults,
24
- verifyWebhookSignature
25
- } from "./chunk-XO36IU66.js";
26
- import "./chunk-4MGIQFAJ.js";
27
-
28
- // src/chainable.ts
29
- var BuilderChainError = class _BuilderChainError extends Error {
30
- missingMethod;
31
- validNext;
32
- constructor(missingMethod, validNext) {
33
- const valid = validNext.length === 0 ? "<chain is terminal>" : validNext.map((m) => `.${m}`).join(", ");
34
- super(`\`.${missingMethod}\` is not a valid step in this builder chain. Valid next: ${valid}.`);
35
- this.name = "BuilderChainError";
36
- this.missingMethod = missingMethod;
37
- this.validNext = validNext;
38
- if (typeof Error.captureStackTrace === "function") {
39
- Error.captureStackTrace(this, _BuilderChainError);
40
- }
1
+ // src/bindings.ts
2
+ function fieldBinding(descriptor, field2) {
3
+ return { __bind: { descriptor, field: field2, kind: "field" }, ref: "" };
4
+ }
5
+ function paramBinding(token) {
6
+ return { __bind: { kind: "param", token }, ref: "" };
7
+ }
8
+ function entityHandle(descriptor, fieldNames) {
9
+ const fields = Object.fromEntries(
10
+ fieldNames.map((field2) => [field2, fieldBinding(descriptor, field2)])
11
+ );
12
+ return { ...fields, __entity: descriptor };
13
+ }
14
+ function isBinding(value2) {
15
+ return typeof value2 === "object" && value2 != null && "__bind" in value2;
16
+ }
17
+ function toSetMap(props) {
18
+ return Object.fromEntries(
19
+ Object.entries(props).map(([key2, value2]) => [key2, toSetValue(value2)])
20
+ );
21
+ }
22
+ function toSetValue(value2) {
23
+ if (value2 === null) {
24
+ return null;
41
25
  }
42
- };
43
- var PASSTHROUGH_PROPS = /* @__PURE__ */ new Set([
44
- "then",
45
- "catch",
46
- "finally",
47
- "toJSON",
48
- "toString",
49
- "valueOf",
50
- "constructor",
51
- "nodeType"
52
- ]);
53
- function chainable(target) {
54
- return new Proxy(target, {
55
- get(t, prop, receiver) {
56
- if (Reflect.has(t, prop)) {
57
- return Reflect.get(t, prop, receiver);
58
- }
59
- if (typeof prop === "symbol") {
60
- return void 0;
61
- }
62
- if (PASSTHROUGH_PROPS.has(prop)) {
63
- return void 0;
64
- }
65
- const validNext = Object.keys(t).filter((k) => typeof Reflect.get(t, k) === "function");
66
- throw new BuilderChainError(prop, validNext);
67
- }
26
+ return isBinding(value2) ? value2 : asPrimitive(value2);
27
+ }
28
+ function isChanged(value2) {
29
+ return typeof value2 === "object" && value2 != null && "kind" in value2 && value2.kind === "changed";
30
+ }
31
+ function toUpdateMap(props) {
32
+ return Object.fromEntries(
33
+ Object.entries(props).map(([key2, value2]) => [
34
+ key2,
35
+ isChanged(value2) ? value2 : toSetValue(value2)
36
+ ])
37
+ );
38
+ }
39
+ function stringValueFromTemplate(strings, values) {
40
+ const segments = strings.flatMap((str, i) => {
41
+ const value2 = values[i];
42
+ const refs = value2 === void 0 ? [] : [value2];
43
+ return [str, ...refs];
68
44
  });
45
+ return { template: segments.filter((seg) => typeof seg !== "string" || seg.length > 0) };
46
+ }
47
+ function asPrimitive(value2) {
48
+ if (typeof value2 === "string" || typeof value2 === "number" || typeof value2 === "boolean") {
49
+ return value2;
50
+ }
51
+ throw new Error("setup set value must be a entity-sourced binding or a literal primitive");
69
52
  }
70
53
 
71
- // src/observer.ts
72
- function createPassOutcome() {
73
- return { kind: "pass" };
54
+ // src/predicates.ts
55
+ function isCapturePredicate(input) {
56
+ return "ref" in input;
74
57
  }
75
- function createRetryOutcome(reason) {
76
- return { kind: "retry", reason };
58
+ function captureOf(predicate) {
59
+ if (predicate.predicate.kind !== "state") {
60
+ throw new Error("internal: a capture predicate must carry a state assertion");
61
+ }
62
+ return { assertion: predicate.predicate.assertion, descriptor: predicate.ref.__entity };
77
63
  }
78
- function createFailOutcome(reason) {
79
- return { kind: "fail", reason };
64
+ function leaf(predicate) {
65
+ return { predicate, wait: (budget) => leaf(withWait(predicate, budget)) };
80
66
  }
81
- function buildReady(state) {
82
- return chainable({
83
- contract: () => makeObserverHandle(state)
84
- });
67
+ function condLeaf(predicate) {
68
+ return {
69
+ __condition: true,
70
+ predicate,
71
+ wait: (budget) => condLeaf(withWaitCondition(predicate, budget))
72
+ };
85
73
  }
86
- function buildNeedsInput(state) {
87
- return chainable({
88
- input: () => buildReady(state)
89
- });
74
+ function isConditionInput(input) {
75
+ return "__condition" in input;
90
76
  }
91
- function buildObserver(name) {
92
- return chainable({
93
- description: (description) => chainable({
94
- budget: (budget) => buildNeedsInput({ budget, description, name })
95
- })
96
- });
77
+ function withWait(predicate, budget) {
78
+ if (predicate.kind === "not" || predicate.kind === "and" || predicate.kind === "count" || predicate.kind === "when") {
79
+ return predicate;
80
+ }
81
+ return { ...predicate, wait: budget };
82
+ }
83
+ function withWaitCondition(condition, budget) {
84
+ return condition.kind === "singleton" ? { ...condition, wait: budget } : condition;
85
+ }
86
+ function toPredicate(input) {
87
+ return "predicate" in input ? input.predicate : input;
88
+ }
89
+ function visible(locator) {
90
+ return leaf({ kind: "visible", locator, wait: void 0 });
91
+ }
92
+ function disabled(locator) {
93
+ return leaf({ kind: "disabled", locator, wait: void 0 });
94
+ }
95
+ function enabled(locator) {
96
+ return leaf({ kind: "enabled", locator, wait: void 0 });
97
+ }
98
+ function focused(locator) {
99
+ return leaf({ kind: "focused", locator, wait: void 0 });
100
+ }
101
+ function value(locator, binding) {
102
+ return leaf({ kind: "value", locator, value: binding, wait: void 0 });
103
+ }
104
+ function text(locator, binding) {
105
+ return leaf({ kind: "text", locator, value: binding, wait: void 0 });
106
+ }
107
+ function not(input) {
108
+ if (isConditionInput(input)) {
109
+ return condLeaf({ kind: "not", predicate: input.predicate });
110
+ }
111
+ return leaf({ kind: "not", predicate: toPredicate(input) });
112
+ }
113
+ function and(...conditions) {
114
+ return condLeaf({ kind: "and", predicates: conditions.map((c) => c.predicate) });
115
+ }
116
+ function count(entity2) {
117
+ return {
118
+ is: (n) => condLeaf({ entity: entity2.name, kind: "count", value: n })
119
+ };
120
+ }
121
+ function when(...clauses) {
122
+ const last = clauses.at(-1);
123
+ const fallback = last !== void 0 && !isClause(last) ? toPredicate(last) : void 0;
124
+ const rows = clauses.filter((clause) => isClause(clause));
125
+ const chain = rows.reduceRight(
126
+ (otherwise, [condition, consequence]) => ({
127
+ condition: condition.predicate,
128
+ consequence: toPredicate(consequence),
129
+ kind: "when",
130
+ otherwise
131
+ }),
132
+ fallback
133
+ );
134
+ if (chain == null) {
135
+ throw new Error("when() needs at least one [condition, consequence] clause");
136
+ }
137
+ return leaf(chain);
138
+ }
139
+ function isClause(clause) {
140
+ return Array.isArray(clause);
97
141
  }
98
142
 
99
- // src/builder.ts
100
- function precondition(name) {
101
- return buildPreconditionStart(name);
143
+ // src/util.ts
144
+ function mapValues(record, fn) {
145
+ return Object.fromEntries(
146
+ Object.entries(record).map(([entryKey, value2]) => [entryKey, fn(value2, entryKey)])
147
+ );
102
148
  }
103
- function observer(name) {
104
- return buildObserver(name);
149
+ function sameSetValue(a, b) {
150
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
151
+ return a === b;
152
+ }
153
+ if ("ref" in a || "ref" in b) {
154
+ return "ref" in a && "ref" in b && a.ref === b.ref;
155
+ }
156
+ return sameTemplate(a, b);
105
157
  }
106
- function test(id, options) {
107
- validateTestId(id);
108
- const sourcePath = captureTestSourcePath();
109
- return buildTestName({ id, sourcePath, uiOnly: options?.uiOnly });
158
+ function sameTemplate(a, b) {
159
+ return a.template.length === b.template.length && a.template.every((segment, index) => sameSegment(segment, b.template[index]));
110
160
  }
111
- var TESTS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]tests[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
112
- function captureTestSourcePath() {
113
- const stack = new Error("capture").stack;
114
- if (stack == null) {
115
- return void 0;
161
+ function sameSegment(a, b) {
162
+ if (typeof a === "string" || typeof b === "string" || b === void 0) {
163
+ return a === b;
116
164
  }
117
- const match = stack.split("\n").map((line) => TESTS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
118
- const captured = match?.[1];
119
- if (captured == null) {
120
- return void 0;
165
+ return a.ref === b.ref;
166
+ }
167
+
168
+ // src/entity.ts
169
+ function stringSpace(generator, constraints) {
170
+ return {
171
+ __t: void 0,
172
+ constraints: constraints == null ? void 0 : { kind: "string", ...constraints },
173
+ generator,
174
+ primitive: "string",
175
+ values: void 0
176
+ };
177
+ }
178
+ function primitiveTypeOf(value2) {
179
+ if (typeof value2 === "number") {
180
+ return "number";
181
+ }
182
+ if (typeof value2 === "boolean") {
183
+ return "boolean";
121
184
  }
122
- return captured.replaceAll("\\", "/");
185
+ return "string";
186
+ }
187
+ var v = {
188
+ boolean: () => ({
189
+ __t: void 0,
190
+ constraints: void 0,
191
+ generator: "lorem.word",
192
+ primitive: "boolean",
193
+ values: void 0
194
+ }),
195
+ companyName: (constraints) => stringSpace("company.name", constraints),
196
+ datetime: (options) => ({
197
+ __t: void 0,
198
+ constraints: {
199
+ kind: "datetime",
200
+ maxOffsetDays: options.offsetDays.max,
201
+ minOffsetDays: options.offsetDays.min
202
+ },
203
+ generator: "date.iso",
204
+ primitive: "string",
205
+ values: void 0
206
+ }),
207
+ email: (constraints) => stringSpace("internet.email", constraints),
208
+ fullName: (constraints) => stringSpace("person.fullName", constraints),
209
+ id: (constraints) => stringSpace("lorem.slug", constraints),
210
+ number: (constraints) => ({
211
+ __t: void 0,
212
+ constraints: { kind: "number", ...constraints },
213
+ generator: "lorem.word",
214
+ primitive: "number",
215
+ values: void 0
216
+ }),
217
+ oneOf: (values) => ({
218
+ __t: void 0,
219
+ constraints: void 0,
220
+ generator: "lorem.word",
221
+ primitive: primitiveTypeOf(values[0]),
222
+ values
223
+ }),
224
+ slug: (constraints) => stringSpace("lorem.slug", constraints),
225
+ url: (constraints) => stringSpace("internet.url", constraints),
226
+ word: (constraints) => stringSpace("lorem.word", constraints)
227
+ };
228
+ function propBuilder(args) {
229
+ return {
230
+ __o: void 0,
231
+ __t: void 0,
232
+ spec: args.spec,
233
+ valueSpace: args.valueSpace
234
+ };
123
235
  }
124
- function createRipplo(registries) {
125
- const { observers, preconditions, tests } = registries;
126
- validateUniqueNames(preconditions, observers, tests);
127
- const preconditionDefs = Object.values(preconditions).map((p) => stubPreconditionDef(p));
128
- const observerDefs = Object.values(observers).map((o) => stubObserverDef(o));
129
- const testDefs = [...tests];
236
+ function field(options) {
237
+ return propBuilder({
238
+ spec: {
239
+ consistency: options.consistency ?? "strict",
240
+ optional: options.optional ?? false,
241
+ stable: options.stable ?? true,
242
+ type: options.value.primitive
243
+ },
244
+ valueSpace: options.value
245
+ });
246
+ }
247
+ function id() {
130
248
  return {
131
- observers,
132
- preconditions,
133
- tests: testDefs,
134
- getObservers: () => observerDefs,
135
- getPreconditions: () => preconditionDefs,
136
- getTests: () => testDefs,
137
- getUnimplemented: () => ({
138
- observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
139
- preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
140
- tests: testDefs.filter((t) => !t.implemented).map((t) => t.id)
249
+ idKind: "surrogate",
250
+ prop: propBuilder({
251
+ spec: { consistency: "strict", optional: false, stable: true, type: "string" },
252
+ valueSpace: void 0
141
253
  })
142
254
  };
143
255
  }
144
- function stubPreconditionDef(p) {
145
- const name = readPreconditionName(p);
256
+ function key(options) {
146
257
  return {
147
- dependsOn: readPreconditionDependsOn(p),
148
- depMapping: readPreconditionDepMapping(p),
149
- description: readPreconditionDescription(p),
150
- implemented: false,
151
- name,
152
- returns: [],
153
- teardown: void 0,
154
- setup: () => Promise.resolve([])
258
+ idKind: "natural",
259
+ prop: propBuilder({
260
+ spec: {
261
+ consistency: "strict",
262
+ optional: false,
263
+ stable: true,
264
+ type: options.value.primitive
265
+ },
266
+ valueSpace: options.value
267
+ })
155
268
  };
156
269
  }
157
- function stubObserverDef(o) {
158
- const name = readObserverName(o);
270
+ function within(selection, sourceField) {
159
271
  return {
160
- budget: readObserverBudget(o),
161
- description: readObserverDescription(o),
162
- implemented: false,
163
- name,
164
- run: () => Promise.resolve(createFailOutcome(`observer "${name}" not implemented`))
272
+ __t: void 0,
273
+ field: String(sourceField),
274
+ kind: "within",
275
+ selection: { entity: selection.entity, where: selection.where }
165
276
  };
166
277
  }
167
- function validateUniqueNames(preconditions, observers, tests) {
168
- const pNames = /* @__PURE__ */ new Set();
169
- Object.values(preconditions).forEach((p) => {
170
- const name = readPreconditionName(p);
171
- if (pNames.has(name)) {
172
- throw new Error(`Duplicate precondition name: "${name}"`);
173
- }
174
- pNames.add(name);
175
- });
176
- const oNames = /* @__PURE__ */ new Set();
177
- Object.values(observers).forEach((o) => {
178
- const name = readObserverName(o);
179
- if (oNames.has(name)) {
180
- throw new Error(`Duplicate observer name: "${name}"`);
181
- }
182
- oNames.add(name);
183
- });
184
- const tIds = /* @__PURE__ */ new Set();
185
- tests.forEach((t) => {
186
- if (tIds.has(t.id)) {
187
- throw new Error(`Duplicate test id: "${t.id}"`);
188
- }
189
- tIds.add(t.id);
190
- });
278
+ function changed() {
279
+ return { kind: "changed" };
191
280
  }
192
- var TEST_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
193
- function validateTestId(id) {
194
- if (!TEST_ID_PATTERN.test(id)) {
195
- throw new Error(
196
- `Invalid test id "${id}". Must be lowercase alphanumeric with hyphens (e.g. "my-test-id").`
197
- );
281
+ function isWithin(value2) {
282
+ return value2 != null && typeof value2 === "object" && "kind" in value2;
283
+ }
284
+ function keyMap(where) {
285
+ return Object.fromEntries(
286
+ Object.entries(where).map(([col, value2]) => [col, keyValue(value2)])
287
+ );
288
+ }
289
+ function keyValue(value2) {
290
+ if (isWithin(value2)) {
291
+ return { field: value2.field, kind: "within", selection: value2.selection };
198
292
  }
293
+ return toSetValue(value2);
294
+ }
295
+ function entity(name, definition) {
296
+ const identityProps = mapValues(definition.identity, (builder) => builder.prop);
297
+ const allBuilders = { ...identityProps, ...definition.fields };
298
+ const fieldKeys = Object.keys(allBuilders);
299
+ const schema = {
300
+ description: definition.description,
301
+ identity: Object.keys(definition.identity),
302
+ identityKind: identityKindOf(definition.identity),
303
+ name,
304
+ props: mapValues(allBuilders, (builder, fieldName) => specWithSpace(name, fieldName, builder)),
305
+ source: definition.source
306
+ };
307
+ const handle = {
308
+ field: mapValues(allBuilders, (builder, fieldName) => ({
309
+ __t: void 0,
310
+ entity: name,
311
+ field: fieldName,
312
+ primitive: builder.spec.type,
313
+ valueSpaceName: `${name}.${fieldName}`
314
+ })),
315
+ name,
316
+ schema,
317
+ source: definition.source,
318
+ valueSpaces: collectValueSpaces(name, allBuilders),
319
+ created: (props) => captureLeaf({
320
+ assertion: { as: "", kind: "created", props: toSetMap(props) },
321
+ entity: name,
322
+ fieldKeys,
323
+ key: {}
324
+ }),
325
+ deleted: (where) => leaf({
326
+ assertion: { kind: "deleted" },
327
+ entity: name,
328
+ key: keyMap(where),
329
+ kind: "state",
330
+ wait: void 0
331
+ }),
332
+ maybe: (where) => makeEntity("maybe", name, fieldKeys, where),
333
+ none: (where) => ({
334
+ __entity: { entity: name, kind: "none", where: toSetMap(where) }
335
+ }),
336
+ of: (props) => makeEntity("of", name, fieldKeys, props),
337
+ only: (props) => makeEntity("only", name, fieldKeys, props),
338
+ updated: (where, props) => captureLeaf({
339
+ assertion: { as: "", kind: "updated", props: toUpdateMap(props) },
340
+ entity: name,
341
+ fieldKeys,
342
+ key: keyMap(where)
343
+ }),
344
+ where: (criteria) => ({
345
+ __s: void 0,
346
+ entity: name,
347
+ where: keyMap(criteria)
348
+ })
349
+ };
350
+ return handle;
199
351
  }
200
- function makePrecondition(params) {
201
- return params;
352
+ function makeEntity(kind, entityName, fieldKeys, props) {
353
+ const descriptor = { entity: entityName, kind, props: toSetMap(props) };
354
+ return entityHandle(descriptor, fieldKeys);
202
355
  }
203
- function buildDepMapping(deps) {
204
- return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
356
+ function matchPropsOf(props) {
357
+ return Object.fromEntries(
358
+ Object.entries(props).filter((entry) => !isChanged(entry[1]))
359
+ );
205
360
  }
206
- function buildPreconditionStart(name) {
207
- let description = "";
208
- const self = chainable({
209
- contract: () => makePrecondition({
210
- dependsOn: [],
211
- depMapping: [],
212
- description,
213
- name
214
- }),
215
- description(text) {
216
- description = text;
217
- return self;
361
+ function captureLeaf(params) {
362
+ const descriptor = {
363
+ entity: params.entity,
364
+ kind: params.assertion.kind,
365
+ props: matchPropsOf(params.assertion.props)
366
+ };
367
+ const ref = entityHandle(descriptor, params.fieldKeys);
368
+ const build = (wait2) => ({
369
+ predicate: {
370
+ assertion: params.assertion,
371
+ entity: params.entity,
372
+ key: params.key,
373
+ kind: "state",
374
+ wait: wait2
218
375
  },
219
- requires(deps) {
220
- return buildPreconditionWithDeps({ deps, name, getDescription: () => description });
221
- }
376
+ ref,
377
+ wait: (budget) => build(budget)
222
378
  });
223
- return self;
379
+ return build(void 0);
224
380
  }
225
- function buildPreconditionWithDeps({
226
- deps,
227
- getDescription,
228
- name
229
- }) {
230
- const description = getDescription();
231
- const depMapping = buildDepMapping(deps);
232
- const dependsOn = depMapping.map(([, depName]) => depName);
233
- return chainable({
234
- contract: () => makePrecondition({
235
- dependsOn,
236
- depMapping,
237
- description,
238
- name
239
- })
240
- });
381
+ function specWithSpace(stateName, fieldName, builder) {
382
+ if (builder.valueSpace == null) {
383
+ return builder.spec;
384
+ }
385
+ return { ...builder.spec, valueSpace: `${stateName}.${fieldName}` };
386
+ }
387
+ function identityKindOf(fields) {
388
+ return Object.values(fields).every((f) => f.idKind === "surrogate") ? "surrogate" : "natural";
389
+ }
390
+ function collectValueSpaces(stateName, builders) {
391
+ return Object.entries(builders).map(([fieldName, builder]) => ({ field: fieldName, partial: builder.valueSpace })).filter(
392
+ (entry) => entry.partial != null
393
+ ).map((entry) => ({
394
+ constraints: entry.partial.constraints,
395
+ generator: entry.partial.generator,
396
+ name: `${stateName}.${entry.field}`,
397
+ type: entry.partial.primitive,
398
+ values: entry.partial.values == null ? void 0 : [...entry.partial.values]
399
+ }));
241
400
  }
242
- function buildTestName({ id, sourcePath, uiOnly }) {
243
- return chainable({
244
- name: (displayName) => buildTestRequires({ id, name: displayName, sourcePath, uiOnly })
245
- });
401
+
402
+ // src/locators.ts
403
+ function role(roleName, name, ...bindings) {
404
+ return { by: "role", name: name == null ? void 0 : nameValue(name, bindings), role: roleName };
246
405
  }
247
- function castOutcome(value) {
248
- return value;
406
+ function inside(scope, target) {
407
+ return { by: "inside", scope, target };
249
408
  }
250
- function buildTestRequires({
251
- id,
252
- name,
253
- sourcePath,
254
- uiOnly
255
- }) {
256
- let description = "";
257
- const self = chainable({
258
- description(text) {
259
- description = text;
260
- return self;
261
- },
262
- requires(reqs) {
263
- const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
264
- const requiresKeys = {};
265
- Object.entries(reqs).forEach(([key, p]) => {
266
- requiresKeys[key] = readPreconditionName(p);
267
- });
268
- return castOutcome(
269
- buildTestOutcome({
270
- id,
271
- name,
272
- reqNames,
273
- requiresKeys,
274
- sourcePath,
275
- uiOnly,
276
- getDescription: () => description
277
- })
278
- );
279
- }
280
- });
281
- return self;
409
+ function testId(value2, ...bindings) {
410
+ if (typeof value2 === "string") {
411
+ return { by: "testId", value: value2 };
412
+ }
413
+ return { by: "testId", value: stringValueFromTemplate(value2, bindings) };
414
+ }
415
+ var alertdialog = named("alertdialog");
416
+ var button = named("button");
417
+ var cell = named("cell");
418
+ var checkbox = named("checkbox");
419
+ var columnheader = named("columnheader");
420
+ var combobox = named("combobox");
421
+ var dialog = named("dialog");
422
+ var heading = named("heading");
423
+ var img = named("img");
424
+ var link = named("link");
425
+ var listitem = named("listitem");
426
+ var menuitem = named("menuitem");
427
+ var option = named("option");
428
+ var radio = named("radio");
429
+ var row = named("row");
430
+ var searchbox = named("searchbox");
431
+ var slider = named("slider");
432
+ var spinbutton = named("spinbutton");
433
+ var switchControl = named("switch");
434
+ var tab = named("tab");
435
+ var textbox = named("textbox");
436
+ var treeitem = named("treeitem");
437
+ var alert = container("alert");
438
+ var banner = container("banner");
439
+ var complementary = container("complementary");
440
+ var contentinfo = container("contentinfo");
441
+ var form = container("form");
442
+ var grid = container("grid");
443
+ var group = container("group");
444
+ var list = container("list");
445
+ var main = container("main");
446
+ var menu = container("menu");
447
+ var navigation = container("navigation");
448
+ var progressbar = container("progressbar");
449
+ var radiogroup = container("radiogroup");
450
+ var region = container("region");
451
+ var status = container("status");
452
+ var table = container("table");
453
+ var tablist = container("tablist");
454
+ var tabpanel = container("tabpanel");
455
+ var toolbar = container("toolbar");
456
+ function named(roleName) {
457
+ return (name, ...bindings) => role(roleName, name, ...bindings);
458
+ }
459
+ function container(roleName) {
460
+ return (name, ...bindings) => role(roleName, name, ...bindings);
461
+ }
462
+ function nameValue(name, bindings) {
463
+ if (typeof name === "string") {
464
+ return name;
465
+ }
466
+ if (isBinding(name)) {
467
+ return name;
468
+ }
469
+ return stringValueFromTemplate(name, bindings);
282
470
  }
283
- function buildTestOutcome({
284
- getDescription,
285
- id,
286
- name,
287
- reqNames,
288
- requiresKeys,
289
- sourcePath,
290
- uiOnly
291
- }) {
292
- const description = getDescription();
293
- return chainable({
294
- expectedOutcome(text) {
295
- return buildTestStartsAt({
296
- description,
297
- expectedOutcome: text,
298
- id,
299
- name,
300
- reqNames,
301
- requiresKeys,
302
- sourcePath,
303
- uiOnly
304
- });
305
- }
306
- });
471
+
472
+ // src/singleton.ts
473
+ function singleton(name, config) {
474
+ const valueSpaceName = `singleton.${name}`;
475
+ const { constraints, generator, primitive } = config.value;
476
+ const schema = {
477
+ consistency: config.consistency ?? "strict",
478
+ default: config.default,
479
+ description: config.description,
480
+ name,
481
+ source: config.source,
482
+ type: primitive,
483
+ valueSpace: valueSpaceName
484
+ };
485
+ return {
486
+ is: isPredicate,
487
+ name,
488
+ schema,
489
+ source: config.source,
490
+ value: { __t: void 0, entity: name, field: "value", primitive, valueSpaceName },
491
+ valueSpaces: [{ constraints, generator, name: valueSpaceName, type: primitive }],
492
+ of: (value2) => ({
493
+ __entity: { kind: "singletonState", singleton: name, value: toSetValue(value2) },
494
+ is: isPredicate
495
+ })
496
+ };
497
+ function isPredicate(value2) {
498
+ return condLeaf({
499
+ assertion: { kind: "is", value: value2 },
500
+ kind: "singleton",
501
+ singleton: name,
502
+ wait: void 0
503
+ });
504
+ }
307
505
  }
308
- function buildTestStartsAt({
309
- description,
310
- expectedOutcome,
311
- id,
312
- name,
313
- reqNames,
314
- requiresKeys,
315
- sourcePath,
316
- uiOnly
317
- }) {
318
- return chainable({
319
- notImplemented: () => ({
320
- coverage: [],
321
- description,
322
- expectedOutcome,
323
- id,
324
- implemented: false,
506
+
507
+ // src/builtins.ts
508
+ function browserSingleton(name) {
509
+ return {
510
+ name,
511
+ is: (strings, ...values) => leaf({
512
+ kind: "browser",
325
513
  name,
326
- requires: [...reqNames],
327
- requiresKeys,
328
- sourcePath,
329
- startsAtFn: void 0,
330
- stepsFn: void 0,
331
- uiOnly
332
- }),
333
- startsAt(fn) {
334
- return buildTestSteps({
335
- description,
336
- expectedOutcome,
337
- id,
338
- name,
339
- reqNames,
340
- requiresKeys,
341
- sourcePath,
342
- startsAtFn: fn,
343
- uiOnly
344
- });
345
- }
346
- });
347
- }
348
- function buildTestSteps({
349
- description,
350
- expectedOutcome,
351
- id,
352
- name,
353
- reqNames,
354
- requiresKeys,
355
- sourcePath,
356
- startsAtFn,
357
- uiOnly
358
- }) {
359
- return chainable({
360
- steps: (stepsFn) => chainable({
361
- coverage: (...ids) => ({
362
- coverage: ids,
363
- description,
364
- expectedOutcome,
365
- id,
366
- implemented: true,
367
- name,
368
- requires: [...reqNames],
369
- requiresKeys,
370
- sourcePath,
371
- startsAtFn,
372
- stepsFn,
373
- uiOnly
374
- })
514
+ value: stringValueFromTemplate(strings, values),
515
+ wait: void 0
375
516
  })
376
- });
517
+ };
377
518
  }
519
+ var url = browserSingleton("url");
520
+ var title = browserSingleton("title");
521
+ var viewport = browserSingleton("viewport");
378
522
 
379
- // src/lint.ts
380
- function lint(result) {
381
- const diagnostics = [];
382
- result.tests.forEach((test2) => {
383
- const nodes = getOrderedNodes(test2);
384
- const report = (diagnostic) => {
385
- diagnostics.push({ ...diagnostic, test: test2.slug });
386
- };
387
- RULES.forEach((rule) => {
388
- rule(nodes, test2, report);
389
- });
390
- });
391
- return { diagnostics };
392
- }
393
- function getOrderedNodes(test2) {
394
- const result = [];
395
- let currentId = test2.spec.entryNode;
396
- const visited = /* @__PURE__ */ new Set();
397
- while (currentId != null && !visited.has(currentId)) {
398
- visited.add(currentId);
399
- const found = test2.spec.nodes[currentId];
400
- if (found == null) {
401
- break;
402
- }
403
- result.push(found);
404
- currentId = found.next;
405
- }
406
- return result;
407
- }
408
- function exactTextMatch(nodes, _test, report) {
409
- nodes.forEach((node) => {
410
- if (node.type === "assertText" && "operator" in node && node.operator !== "equals") {
411
- report({
412
- message: `${node.type} uses operator "${node.operator}" \u2014 only "equals" is allowed for determinism`,
413
- rule: "exact-text-match",
414
- step: node.label ?? node.id
415
- });
416
- }
417
- });
523
+ // src/actions.ts
524
+ function goto(strings, ...values) {
525
+ return stepBuilder({ kind: "goto", url: stringValueFromTemplate(strings, values) }, [], []);
418
526
  }
419
- function noHardcodedData(nodes, test2, report) {
420
- const hasVariables = Object.keys(test2.spec.variables ?? {}).length > 0;
421
- if (!hasVariables) {
422
- return;
423
- }
424
- nodes.forEach((node) => {
425
- if (node.type === "fill" && isStaticStringValue(node.value)) {
426
- const val = node.value.value;
427
- if (!isTemplateVar(val) && looksLikeDynamicData(val)) {
428
- report({
429
- message: `fill() uses hardcoded value "${val}" \u2014 consider using precondition data via {{namespace.key}}`,
430
- rule: "no-hardcoded-data",
431
- step: node.label ?? node.id
432
- });
433
- }
434
- }
435
- });
527
+ function click(locator) {
528
+ return stepBuilder({ kind: "click", locator }, [], []);
436
529
  }
437
- function preferPreconditionData(nodes, test2, report) {
438
- const variableKeys = Object.keys(test2.spec.variables ?? {});
439
- if (variableKeys.length === 0) {
440
- return;
441
- }
442
- const hasAnyTemplateRef = nodes.some((node) => JSON.stringify(node).includes("{{"));
443
- if (!hasAnyTemplateRef) {
444
- report({
445
- message: "Test requires preconditions but steps() never references precondition data \u2014 destructure and use it",
446
- rule: "prefer-precondition-data",
447
- step: void 0
448
- });
449
- }
530
+ function dblclick(locator) {
531
+ return stepBuilder({ kind: "dblclick", locator }, [], []);
450
532
  }
451
- function missingLabel(nodes, _test, report) {
452
- nodes.forEach((node) => {
453
- if (node.label == null || node.label.length === 0) {
454
- report({
455
- message: `Step "${node.id}" lacks .as("...") label \u2014 every step must be labeled`,
456
- rule: "missing-label",
457
- step: node.id
458
- });
459
- }
460
- });
533
+ function fill(locator, binding) {
534
+ return stepBuilder({ kind: "fill", locator, value: binding }, [], []);
461
535
  }
462
- function noDuplicateLabels(nodes, _test, report) {
463
- const seen = /* @__PURE__ */ new Map();
464
- nodes.forEach((node) => {
465
- if (node.label == null) {
466
- return;
467
- }
468
- const existing = seen.get(node.label);
469
- if (existing == null) {
470
- seen.set(node.label, node.id);
471
- } else {
472
- report({
473
- message: `Duplicate label "${node.label}" \u2014 also used by ${existing}`,
474
- rule: "no-duplicate-labels",
475
- step: node.label
476
- });
477
- }
478
- });
536
+ function clear(locator) {
537
+ return stepBuilder({ kind: "clear", locator }, [], []);
479
538
  }
480
- function assertAfterAction(nodes, _test, report) {
481
- let consecutiveActions = 0;
482
- nodes.forEach((node, index) => {
483
- if (index === 0 && node.type === "goto") {
484
- return;
485
- }
486
- if (isAssertionNode(node)) {
487
- consecutiveActions = 0;
488
- return;
489
- }
490
- consecutiveActions++;
491
- if (consecutiveActions === 3) {
492
- report({
493
- message: "3+ consecutive actions without an assertion \u2014 add verification between actions",
494
- rule: "assert-after-action",
495
- step: node.label ?? node.id
496
- });
497
- }
498
- });
539
+ function select(locator, binding) {
540
+ return stepBuilder({ kind: "select", locator, value: binding }, [], []);
499
541
  }
500
- function assertMatchesOutcome(nodes, _test, report) {
501
- if (nodes.length === 0) {
502
- return;
503
- }
504
- const lastNode = nodes.at(-1);
505
- if (lastNode != null && !isAssertionNode(lastNode)) {
506
- report({
507
- message: "Last step is an action, not an assertion \u2014 expectedOutcome should be verified by assertions at the end",
508
- rule: "assert-matches-outcome",
509
- step: lastNode.label ?? lastNode.id
510
- });
511
- }
542
+ function check(locator) {
543
+ return stepBuilder({ kind: "check", locator }, [], []);
512
544
  }
513
- function noEmptySteps(nodes, test2, report) {
514
- if (!test2.implemented) {
515
- return;
516
- }
517
- if (nodes.length === 0) {
518
- report({
519
- message: "Test has zero steps",
520
- rule: "no-empty-steps",
521
- step: void 0
522
- });
523
- }
545
+ function uncheck(locator) {
546
+ return stepBuilder({ kind: "uncheck", locator }, [], []);
524
547
  }
525
- function isStaticStringValue(val) {
526
- if (typeof val !== "object" || val == null) {
527
- return false;
528
- }
529
- if (!("type" in val) || val.type !== "static") {
530
- return false;
531
- }
532
- if (!("value" in val) || typeof val.value !== "string") {
533
- return false;
534
- }
535
- return true;
548
+ function hover(locator) {
549
+ return stepBuilder({ kind: "hover", locator }, [], []);
536
550
  }
537
- function isTemplateVar(value) {
538
- return value.includes("{{");
551
+ function upload(locator, files) {
552
+ return stepBuilder({ files: [...files], kind: "upload", locator }, [], []);
539
553
  }
540
- function looksLikeDynamicData(value) {
541
- return value.includes("@") || /\b[a-f0-9]{8,}\b/.test(value) || /^(test|ripplo|example|sample|demo)/i.test(value);
554
+ function press(pressKey, locator) {
555
+ return stepBuilder({ key: pressKey, kind: "press", locator }, [], []);
542
556
  }
543
- function isAssertionNode(node) {
544
- return node.type.startsWith("assert");
557
+ function stepBuilder(action, expected, captures) {
558
+ return {
559
+ captures,
560
+ step: { action, expect: [...expected] },
561
+ expect: (...predicates) => stepBuilder(
562
+ action,
563
+ [...expected, ...predicates.map((p) => toPredicate(p))],
564
+ [...captures, ...predicates.flatMap((p) => isCapturePredicate(p) ? [captureOf(p)] : [])]
565
+ )
566
+ };
545
567
  }
546
- var EFFECT_ASSERTION_TYPES = /* @__PURE__ */ new Set([
547
- "assertText",
548
- "assertValue",
549
- "assertCount",
550
- "assertUrl",
551
- "assertNotVisible",
552
- "assertChecked",
553
- "assertNotChecked",
554
- "assertAttribute",
555
- "assertDisabled",
556
- "assertEnabled"
557
- ]);
558
- function isEffectAssertion(node) {
559
- return EFFECT_ASSERTION_TYPES.has(node.type);
568
+
569
+ // src/finalize.ts
570
+ function finalize(body) {
571
+ const descriptors = [...new Set(body.given.map((item) => item.__entity))];
572
+ const absences = descriptors.filter((d) => d.kind === "none");
573
+ const singletonStates = descriptors.filter(
574
+ (d) => d.kind === "singletonState"
575
+ );
576
+ const entities = topoSort(
577
+ descriptors.filter(
578
+ (d) => d.kind === "of" || d.kind === "only" || d.kind === "maybe"
579
+ )
580
+ );
581
+ assertNoMaybeRefs({ absences, entities, singletonStates });
582
+ const captures = body.steps.flatMap((builder) => builder.captures);
583
+ assertScopedConditions(body.steps, new Set(singletonStates.map((s) => s.singleton)));
584
+ const aliases = assignAliases([...entities, ...captures.map((c) => c.descriptor)]);
585
+ const { names, params } = assignParams(allBindings(body, entities, absences, singletonStates));
586
+ const ctx = {
587
+ aliases,
588
+ captures: new Map(
589
+ captures.map((c) => [c.assertion, c.descriptor])
590
+ ),
591
+ params: names
592
+ };
593
+ return {
594
+ absent: absences.map((a) => ({ entity: a.entity, where: setMap(a.where, ctx) })),
595
+ exclusive: [...new Set(entities.filter((d) => d.kind === "only").map((d) => d.entity))],
596
+ maybe: setupsOf(entities, "maybe", ctx),
597
+ params,
598
+ singletons: resolveSingletons(singletonStates, ctx),
599
+ steps: body.steps.map((builder) => resolveStep(builder.step, ctx)),
600
+ world: entities.filter((d) => d.kind === "of" || d.kind === "only").map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }))
601
+ };
560
602
  }
561
- function noAssertions(nodes, test2, report) {
562
- if (!test2.implemented || nodes.length === 0) {
603
+ function resolveSingletons(states, ctx) {
604
+ return states.reduce((acc, state) => {
605
+ const value2 = resolveValue(state.value, ctx);
606
+ const existing = acc[state.singleton];
607
+ if (existing !== void 0 && !sameSetValue(existing, value2)) {
608
+ throw new Error(
609
+ `singleton "${state.singleton}" is given two conflicting values \u2014 a test may set each singleton to one value`
610
+ );
611
+ }
612
+ return { ...acc, [state.singleton]: value2 };
613
+ }, {});
614
+ }
615
+ function assertNoMaybeRefs({ absences, entities, singletonStates }) {
616
+ const offenders = [
617
+ ...entities.filter((d) => d.kind === "of" || d.kind === "only").flatMap((d) => maybeBindings(Object.values(d.props), `${d.kind}(${d.entity})`)),
618
+ ...absences.flatMap((a) => maybeBindings(Object.values(a.where), `none(${a.entity})`)),
619
+ ...singletonStates.flatMap((s) => maybeBindings([s.value], `singleton "${s.singleton}"`))
620
+ ];
621
+ const first = offenders[0];
622
+ if (first == null || first.binding.__bind.kind !== "field") {
563
623
  return;
564
624
  }
565
- if (!nodes.some((n) => isAssertionNode(n))) {
566
- report({
567
- message: "Test has zero assertion steps \u2014 cannot verify expectedOutcome. Add assert.* steps.",
568
- rule: "no-assertions",
569
- step: void 0
570
- });
571
- }
625
+ const { descriptor, field: field2 } = first.binding.__bind;
626
+ throw new Error(
627
+ `${first.site} references ${descriptor.entity}.${field2} from a maybe(${descriptor.entity}) \u2014 maybe entities are not materialized at setup; reference them only in steps`
628
+ );
572
629
  }
573
- function lowAssertionRatio(nodes, test2, report) {
574
- if (!test2.implemented || nodes.length <= 3) {
575
- return;
576
- }
577
- const assertions = nodes.filter((n) => isAssertionNode(n)).length;
578
- if (assertions === 0) {
579
- return;
580
- }
581
- const ratio = assertions / nodes.length;
582
- if (ratio < 0.15) {
583
- const pct = Math.round(ratio * 100);
584
- report({
585
- message: `Only ${String(assertions)}/${String(nodes.length)} steps are assertions (${String(pct)}%) \u2014 add more verification between actions`,
586
- rule: "low-assertion-ratio",
587
- step: void 0
588
- });
589
- }
630
+ function maybeBindings(values, site) {
631
+ return values.flatMap((value2) => valueBindings(value2)).filter((b) => b.__bind.kind === "field" && b.__bind.descriptor.kind === "maybe").map((binding) => ({ binding, site }));
590
632
  }
591
- function locatorSignature(node) {
592
- if (!("locator" in node) || node.locator == null) {
593
- return void 0;
633
+ function valueBindings(value2) {
634
+ if (isBinding(value2)) {
635
+ return [value2];
594
636
  }
595
- const loc = node.locator;
596
- if (loc.by === "role") {
597
- return `role:${loc.role}:${loc.name ?? ""}`;
637
+ if (isTemplate(value2)) {
638
+ return value2.template.flatMap((segment) => isBinding(segment) ? [segment] : []);
598
639
  }
599
- return `testId:${loc.value}`;
640
+ return [];
600
641
  }
601
- function tautologicalPostClickAssert(nodes, _test, report) {
602
- nodes.forEach((node, index) => {
603
- if (node.type !== "click") {
604
- return;
605
- }
606
- const sig = locatorSignature(node);
607
- if (sig == null) {
608
- return;
609
- }
610
- const window = nodes.slice(index + 1, index + 4);
611
- const matchesSameAssertVisible = window.find(
612
- (n) => n.type === "assertVisible" && locatorSignature(n) === sig
613
- );
614
- if (matchesSameAssertVisible == null) {
615
- return;
616
- }
617
- const hasOtherEffect = window.some(
618
- (n) => isEffectAssertion(n) || isAssertionNode(n) && locatorSignature(n) !== sig
619
- );
620
- if (hasOtherEffect) {
621
- return;
622
- }
623
- report({
624
- message: `click "${node.label ?? node.id}" is followed only by assert.visible on the same locator \u2014 verifies nothing about the click's effect`,
625
- rule: "tautological-post-click-assert",
626
- step: node.label ?? node.id
627
- });
628
- });
642
+ function isTemplate(value2) {
643
+ return typeof value2 === "object" && value2 !== null && "template" in value2;
629
644
  }
630
- var STOPWORDS = /* @__PURE__ */ new Set([
631
- "the",
632
- "is",
633
- "a",
634
- "an",
635
- "and",
636
- "or",
637
- "of",
638
- "to",
639
- "in",
640
- "on",
641
- "at",
642
- "for",
643
- "that",
644
- "this",
645
- "with",
646
- "be",
647
- "are",
648
- "was",
649
- "were",
650
- "it",
651
- "its",
652
- "as",
653
- "by",
654
- "from",
655
- "after",
656
- "before",
657
- "should",
658
- "will",
659
- "can",
660
- "still",
661
- "but",
662
- "not",
663
- "no",
664
- "so",
665
- "if",
666
- "then",
667
- "than",
668
- "user",
669
- "users",
670
- "page",
671
- "view",
672
- "shows",
673
- "show",
674
- "see",
675
- "click",
676
- "clicks",
677
- "clicked",
678
- "clickable",
679
- "visible",
680
- "appears",
681
- "appear",
682
- "displayed",
683
- "stays",
684
- "remains"
685
- ]);
686
- function tokenize(text) {
687
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOPWORDS.has(t));
645
+ function topoSort(entities) {
646
+ return emitReady([], entities);
688
647
  }
689
- function nodeKeywords(node) {
690
- const fromLabel = node.label == null ? [] : tokenize(node.label);
691
- if (!("locator" in node) || node.locator == null) {
692
- return fromLabel;
648
+ function emitReady(done, remaining) {
649
+ if (remaining.length === 0) {
650
+ return [...done];
693
651
  }
694
- const loc = node.locator;
695
- const locName = loc.by === "role" ? loc.name ?? "" : loc.value;
696
- return [...fromLabel, ...tokenize(locName)];
652
+ const ready = remaining.filter(
653
+ (d) => depsOf(d, remaining).every((dep) => done.includes(dep) || dep === d)
654
+ );
655
+ if (ready.length === 0) {
656
+ throw new Error("cyclic dependency between given entities");
657
+ }
658
+ return emitReady(
659
+ [...done, ...ready],
660
+ remaining.filter((d) => !ready.includes(d))
661
+ );
697
662
  }
698
- function expectedOutcomeKeywordCoverage(nodes, test2, report) {
699
- if (!test2.implemented || nodes.length === 0) {
663
+ function depsOf(descriptor, among) {
664
+ return Object.values(descriptor.props).flatMap((v2) => valueBindings(v2)).flatMap((b) => b.__bind.kind === "field" ? [b.__bind.descriptor] : []).filter((target) => target !== descriptor && among.includes(target));
665
+ }
666
+ function assertScopedConditions(steps, givenSingletons) {
667
+ steps.flatMap((builder) => builder.step.expect).forEach((predicate) => {
668
+ assertPredicateScoped(predicate, givenSingletons);
669
+ });
670
+ }
671
+ function assertPredicateScoped(predicate, given) {
672
+ if (predicate.kind === "not") {
673
+ assertPredicateScoped(predicate.predicate, given);
700
674
  return;
701
675
  }
702
- const outcomeTokens = new Set(tokenize(test2.expectedOutcome));
703
- if (outcomeTokens.size === 0) {
676
+ if (predicate.kind !== "when") {
704
677
  return;
705
678
  }
706
- const assertions = nodes.filter((n) => isAssertionNode(n));
707
- if (assertions.length === 0) {
708
- return;
679
+ conditionSingletons(predicate.condition).forEach((name) => {
680
+ if (!given.has(name)) {
681
+ throw new Error(
682
+ `when() conditions on singleton "${name}", which is not in the test's given \u2014 add ${name}.of(...) to given`
683
+ );
684
+ }
685
+ });
686
+ assertPredicateScoped(predicate.consequence, given);
687
+ }
688
+ function conditionSingletons(predicate) {
689
+ if (predicate.kind === "singleton") {
690
+ return [predicate.singleton];
709
691
  }
710
- const covered = assertions.some(
711
- (node) => nodeKeywords(node).some((tok) => outcomeTokens.has(tok))
712
- );
713
- if (!covered) {
714
- report({
715
- message: `No assertion references any keyword from expectedOutcome (${[...outcomeTokens].join(", ")}) \u2014 the outcome may not actually be verified`,
716
- rule: "expected-outcome-keyword-coverage",
717
- step: void 0
718
- });
692
+ if (predicate.kind === "not") {
693
+ return conditionSingletons(predicate.predicate);
694
+ }
695
+ if (predicate.kind === "and") {
696
+ return predicate.predicates.flatMap((p) => conditionSingletons(p));
719
697
  }
698
+ return [];
699
+ }
700
+ function assignAliases(entities) {
701
+ return entities.reduce(
702
+ (acc, d) => {
703
+ const ordinal = acc.counts[d.entity] ?? 0;
704
+ return {
705
+ aliases: new Map([...acc.aliases, [d, `${d.entity}_${String(ordinal)}`]]),
706
+ counts: { ...acc.counts, [d.entity]: ordinal + 1 }
707
+ };
708
+ },
709
+ { aliases: /* @__PURE__ */ new Map(), counts: {} }
710
+ ).aliases;
711
+ }
712
+ function assignParams(bindings) {
713
+ const unique = [...new Set(bindings.filter((b) => b.__bind.kind === "param"))];
714
+ const assigned = unique.reduce(
715
+ (acc, b) => {
716
+ const token = tokenOf(b);
717
+ const ordinal = acc.counts[token.base] ?? 0;
718
+ const name = ordinal === 0 ? token.base : `${token.base}_${String(ordinal - 1)}`;
719
+ if (name in acc.params) {
720
+ throw new Error(
721
+ `param name "${name}" collides with an existing param \u2014 rename the field so deduplicated param names stay unique`
722
+ );
723
+ }
724
+ return {
725
+ counts: { ...acc.counts, [token.base]: ordinal + 1 },
726
+ names: new Map([...acc.names, [b, name]]),
727
+ params: { ...acc.params, [name]: { example: token.example, valueSpace: token.valueSpace } }
728
+ };
729
+ },
730
+ { counts: {}, names: /* @__PURE__ */ new Map(), params: {} }
731
+ );
732
+ return { names: assigned.names, params: assigned.params };
720
733
  }
721
- var BACKEND_MUTATION_KEYWORDS = /save|submit|create|delete|remove|send|invite|update|confirm|publish|apply|pay|subscribe|upgrade|cancel|archive|rename/i;
722
- function isLikelyBackendMutation(node) {
723
- if (node.type === "upload") {
724
- return true;
734
+ function tokenOf(binding) {
735
+ if (binding.__bind.kind !== "param") {
736
+ throw new Error("internal: expected a param binding");
737
+ }
738
+ return binding.__bind.token;
739
+ }
740
+ function allBindings(body, entities, absences, singletonStates) {
741
+ return [
742
+ ...entities.flatMap((d) => Object.values(d.props).flatMap((v2) => valueBindings(v2))),
743
+ ...absences.flatMap((a) => Object.values(a.where).flatMap((v2) => valueBindings(v2))),
744
+ ...singletonStates.flatMap((s) => valueBindings(s.value)),
745
+ ...body.steps.flatMap((builder) => stepBindings(builder.step))
746
+ ];
747
+ }
748
+ function stepBindings(step) {
749
+ return [...actionBindings(step.action), ...step.expect.flatMap((p) => predicateBindings(p))];
750
+ }
751
+ function actionBindings(action) {
752
+ if (action.kind === "goto") {
753
+ return valueBindings(action.url);
754
+ }
755
+ if (action.kind === "fill" || action.kind === "select") {
756
+ return [...locatorBindings(action.locator), ...valueBindings(action.value)];
757
+ }
758
+ if (action.kind === "press") {
759
+ return action.locator == null ? [] : locatorBindings(action.locator);
725
760
  }
726
- if (node.type !== "click") {
727
- return false;
761
+ if (action.kind === "upload") {
762
+ return locatorBindings(action.locator);
728
763
  }
729
- const loc = node.locator;
730
- const name = loc.by === "role" ? loc.name ?? "" : loc.value;
731
- return BACKEND_MUTATION_KEYWORDS.test(name);
764
+ return locatorBindings(action.locator);
732
765
  }
733
- function mutationWithoutObserverCoverage(nodes, test2, report) {
734
- if (!test2.implemented || test2.spec.uiOnly === true) {
735
- return;
766
+ function locatorBindings(locator) {
767
+ if (locator.by === "inside") {
768
+ return [...locatorBindings(locator.scope), ...locatorBindings(locator.target)];
736
769
  }
737
- nodes.forEach((node, index) => {
738
- if (!isLikelyBackendMutation(node)) {
739
- return;
770
+ if (locator.by === "role") {
771
+ return locator.name == null ? [] : valueBindings(locator.name);
772
+ }
773
+ return valueBindings(locator.value);
774
+ }
775
+ function predicateBindings(predicate) {
776
+ switch (predicate.kind) {
777
+ case "visible":
778
+ case "disabled":
779
+ case "enabled":
780
+ case "focused": {
781
+ return locatorBindings(predicate.locator);
740
782
  }
741
- if ("uiOnly" in node && node.uiOnly === true) {
742
- return;
783
+ case "value":
784
+ case "text": {
785
+ return [...locatorBindings(predicate.locator), ...valueBindings(predicate.value)];
743
786
  }
744
- const rest = nodes.slice(index + 1);
745
- const nextMutationIdx = rest.findIndex((n) => isLikelyBackendMutation(n));
746
- const windowEnd = nextMutationIdx === -1 ? rest.length : nextMutationIdx;
747
- const window = rest.slice(0, windowEnd);
748
- const hasObserver = window.some((n) => n.type === "assertObserver");
749
- if (hasObserver) {
750
- return;
787
+ case "singleton": {
788
+ return valueBindings(predicate.assertion.value);
751
789
  }
752
- report({
753
- message: `"${node.label ?? node.id}" looks like it mutates backend state but no assert.backend(...) observer verifies it \u2014 the test can pass while the server is broken. You need to add an observer that checks the persisted result (see .ripplo/observers/ for examples). Only if this action truly does NOT change any server state (pure client-side interaction, presentation toggle, etc.) should you fall back to marking the step { uiOnly: true } to silence this rule.`,
754
- rule: "mutation-without-observer-coverage",
755
- step: node.label ?? node.id
756
- });
757
- });
758
- }
759
- function observerParamsReferenceVariables(nodes, test2, report) {
760
- const variableKeys = Object.keys(test2.spec.variables ?? {});
761
- if (variableKeys.length === 0) {
762
- return;
763
- }
764
- nodes.forEach((node) => {
765
- if (node.type !== "assertObserver") {
766
- return;
790
+ case "browser": {
791
+ return valueBindings(predicate.value);
767
792
  }
768
- const paramEntries = Object.entries(node.params);
769
- if (paramEntries.length === 0) {
770
- return;
793
+ case "state": {
794
+ return [
795
+ ...predicate.assertion.kind === "deleted" ? [] : Object.values(predicate.assertion.props).flatMap(
796
+ (v2) => isChanged(v2) ? [] : valueBindings(v2)
797
+ ),
798
+ ...Object.values(predicate.key).flatMap((v2) => whereBindings(v2))
799
+ ];
771
800
  }
772
- const anyReferencesVariable = paramEntries.some(([, ref]) => {
773
- return isStaticStringValue(ref) && isTemplateVar(ref.value);
774
- });
775
- if (anyReferencesVariable) {
776
- return;
801
+ case "not": {
802
+ return predicateBindings(predicate.predicate);
777
803
  }
778
- report({
779
- message: `assert.backend "${node.label ?? node.id}" passes only hardcoded params while the test defines precondition variables \u2014 observer assertions should reference {{namespace.key}} so they match the actual precondition data`,
780
- rule: "observer-params-reference-variables",
781
- step: node.label ?? node.id
782
- });
783
- });
784
- }
785
- function uploadFixtureName(nodes, _test, report) {
786
- nodes.forEach((node) => {
787
- if (node.type !== "upload") {
788
- return;
804
+ case "when": {
805
+ return [
806
+ ...predicateBindings(predicate.condition),
807
+ ...predicateBindings(predicate.consequence),
808
+ ...predicate.otherwise == null ? [] : predicateBindings(predicate.otherwise)
809
+ ];
789
810
  }
790
- node.files.forEach((name) => {
791
- if (name.length === 0) {
792
- report({
793
- message: `upload "${node.label ?? node.id}" references an empty fixture name`,
794
- rule: "upload-fixture-name",
795
- step: node.label ?? node.id
796
- });
797
- return;
798
- }
799
- if (name.includes("..") || name.startsWith("/")) {
800
- report({
801
- message: `upload "${node.label ?? node.id}" references "${name}" \u2014 fixture names must be relative paths under .ripplo/fixtures/, no ".." or absolute paths`,
802
- rule: "upload-fixture-name",
803
- step: node.label ?? node.id
804
- });
805
- }
806
- });
807
- });
808
- }
809
- function noLiteralTemplateStrings(nodes, test2, report) {
810
- const declaredKeys = new Set(Object.keys(test2.spec.variables ?? {}));
811
- const pattern = /\{\{([^{}]+?)\}\}/g;
812
- nodes.forEach((node) => {
813
- const serialized = JSON.stringify(node);
814
- const allMatches = [...serialized.matchAll(pattern)];
815
- const undeclared = [
816
- ...new Set(
817
- allMatches.map((m) => m[1]).filter((key) => key != null && !declaredKeys.has(key))
818
- )
819
- ];
820
- if (undeclared.length === 0) {
821
- return;
811
+ case "and": {
812
+ return predicate.predicates.flatMap((p) => predicateBindings(p));
822
813
  }
823
- const keyList = undeclared.map((k) => `{{${k}}}`).join(", ");
824
- const namespaces = [...new Set(undeclared.map((k) => k.split(".")[0] ?? k))].join(", ");
825
- report({
826
- message: `"${node.label ?? node.id}" contains literal template string(s) ${keyList} \u2014 destructure the proxy in .steps(({ ${namespaces} }) => \u2026) and pass the proxy value (e.g. \`${undeclared[0] ?? ""}\`) directly. Writing the template as a string bypasses type-checking and silently accepts typos.`,
827
- rule: "no-literal-template-strings",
828
- step: node.label ?? node.id
829
- });
830
- });
814
+ case "count": {
815
+ return [];
816
+ }
817
+ }
831
818
  }
832
- var RULES = [
833
- exactTextMatch,
834
- noHardcodedData,
835
- noLiteralTemplateStrings,
836
- preferPreconditionData,
837
- missingLabel,
838
- noDuplicateLabels,
839
- assertAfterAction,
840
- assertMatchesOutcome,
841
- noEmptySteps,
842
- noAssertions,
843
- lowAssertionRatio,
844
- tautologicalPostClickAssert,
845
- expectedOutcomeKeywordCoverage,
846
- mutationWithoutObserverCoverage,
847
- observerParamsReferenceVariables,
848
- uploadFixtureName
849
- ];
850
-
851
- // src/engine.ts
852
- var TEST_ID_PREFIX = "ripplo-test-";
853
- function notImplemented(reason) {
854
- return { reason: reason ?? "not implemented" };
855
- }
856
- function isNotImplemented(value) {
857
- return typeof value === "object" && value !== null && "reason" in value && Object.keys(value).length === 1;
858
- }
859
- function createEngine(ripplo, impls) {
860
- const preconditionDefs = wirePreconditions(ripplo.preconditions, impls.preconditions);
861
- const observerDefs = wireObservers(ripplo.observers, impls.observers);
862
- const preconditionsByName = new Map(preconditionDefs.map((d) => [d.name, d]));
863
- const observersByName = new Map(observerDefs.map((d) => [d.name, d]));
864
- return {
865
- executeObserver: (name, params) => executeObserver(observersByName, name, params),
866
- executePreconditions: (items, options) => executePreconditions({ defsByName: preconditionsByName, items, options }),
867
- getObservers: () => observerDefs,
868
- getPreconditions: () => preconditionDefs,
869
- getUnimplemented: () => ({
870
- observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
871
- preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
872
- tests: ripplo.tests.filter((t) => !t.implemented).map((t) => t.id)
873
- }),
874
- teardown: (items) => teardown({ defsByName: preconditionsByName, items })
875
- };
819
+ function whereBindings(value2) {
820
+ return isWithin2(value2) ? Object.values(value2.selection.where).flatMap((v2) => whereBindings(v2)) : valueBindings(value2);
876
821
  }
877
- function wirePreconditions(registry, impls) {
878
- return Object.entries(registry).map(([key, handle]) => {
879
- const impl = Reflect.get(impls, key);
880
- if (isNotImplemented(impl)) {
881
- return makeNotImplementedPreconditionDef(handle, impl);
882
- }
883
- return makeWiredPreconditionDef(handle, impl);
884
- });
822
+ function isWithin2(value2) {
823
+ return typeof value2 === "object" && value2 !== null && "kind" in value2;
824
+ }
825
+ function setMap(map, ctx) {
826
+ return Object.fromEntries(
827
+ Object.entries(map).map(([key2, value2]) => [key2, resolveValue(value2, ctx)])
828
+ );
885
829
  }
886
- function wireObservers(registry, impls) {
887
- return Object.entries(registry).map(([key, handle]) => {
888
- const impl = Reflect.get(impls, key);
889
- if (isNotImplemented(impl)) {
890
- return makeNotImplementedObserverDef(handle, impl);
830
+ function resolveValue(value2, ctx) {
831
+ if (isBinding(value2)) {
832
+ return resolveBinding(value2, ctx);
833
+ }
834
+ if (isTemplate(value2)) {
835
+ return resolveTemplate(value2, ctx);
836
+ }
837
+ return value2;
838
+ }
839
+ function resolveBinding(binding, ctx) {
840
+ const bind = binding.__bind;
841
+ if (bind.kind === "param") {
842
+ const name = ctx.params.get(binding);
843
+ if (name == null) {
844
+ throw new Error("internal: param binding was not collected");
891
845
  }
892
- return makeWiredObserverDef(handle, impl);
893
- });
846
+ return { ref: name };
847
+ }
848
+ const alias = ctx.aliases.get(bind.descriptor);
849
+ if (alias == null) {
850
+ throw new Error(
851
+ `references a "${bind.descriptor.entity}" entity that is not included in the test's given`
852
+ );
853
+ }
854
+ return { ref: `${alias}.${bind.field}` };
894
855
  }
895
- function makeNotImplementedPreconditionDef(handle, sentinel) {
896
- const name = readPreconditionName(handle);
856
+ function resolveTemplate(template, ctx) {
897
857
  return {
898
- dependsOn: readPreconditionDepMappingKeys(handle),
899
- depMapping: readPreconditionDepMapping(handle),
900
- description: readDescriptionOf(handle),
901
- implemented: false,
902
- name,
903
- returns: [],
904
- teardown: void 0,
905
- setup: () => Promise.reject(new Error(`Precondition "${name}" is ${sentinel.reason}`))
858
+ template: template.template.map(
859
+ (segment) => isBinding(segment) ? resolveBinding(segment, ctx) : segment
860
+ )
906
861
  };
907
862
  }
908
- function makeWiredPreconditionDef(handle, impl) {
909
- const name = readPreconditionName(handle);
910
- const mapping = readPreconditionDepMapping(handle);
911
- const typedImpl = impl;
912
- return {
913
- dependsOn: mapping.map(([, depName]) => depName),
914
- depMapping: mapping,
915
- description: readDescriptionOf(handle),
916
- implemented: true,
917
- name,
918
- returns: [],
919
- setup: async (items) => {
920
- const resolvedItems = items.map((item) => {
921
- const resolved = {};
922
- mapping.forEach(([key, depName]) => {
923
- const data = item.deps[depName];
924
- if (data != null) {
925
- resolved[key] = data;
926
- }
927
- });
928
- return { ctx: item.ctx, deps: resolved };
929
- });
930
- const results = await typedImpl.setup(resolvedItems);
931
- return results.map((r) => {
932
- const out = {};
933
- Object.entries(r).forEach(([key, value]) => {
934
- if (!isPrimitive(value)) {
935
- throw new TypeError(
936
- `Precondition "${name}" returned non-primitive value at key "${key}". Setup return values must be string, number, or boolean \u2014 produced via ctx.fixed/uniqueEmail/uniqueId.`
937
- );
938
- }
939
- out[key] = value;
940
- });
941
- return out;
942
- });
943
- },
944
- teardown: async (items) => {
945
- const typed = items;
946
- await typedImpl.teardown(typed);
947
- }
948
- };
863
+ function setupsOf(entities, kind, ctx) {
864
+ return entities.filter((d) => d.kind === kind).map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }));
949
865
  }
950
- function makeNotImplementedObserverDef(handle, sentinel) {
951
- const name = readObserverName(handle);
952
- return {
953
- budget: readBudgetOf(handle),
954
- description: readDescriptionOfObserver(handle),
955
- implemented: false,
956
- name,
957
- run: () => Promise.resolve(createFailOutcome(`observer "${name}" is ${sentinel.reason}`))
958
- };
866
+ function aliasFor(ctx, descriptor) {
867
+ const alias = ctx.aliases.get(descriptor);
868
+ if (alias == null) {
869
+ throw new Error(`internal: no alias for ${descriptor.entity}`);
870
+ }
871
+ return alias;
959
872
  }
960
- function makeWiredObserverDef(handle, impl) {
961
- const name = readObserverName(handle);
962
- const typedImpl = impl;
873
+ function resolveStep(step, ctx) {
963
874
  return {
964
- budget: readBudgetOf(handle),
965
- description: readDescriptionOfObserver(handle),
966
- implemented: true,
967
- name,
968
- run: async (ctx, params) => {
969
- return typedImpl(ctx, params);
970
- }
875
+ action: resolveAction(step.action, ctx),
876
+ expect: step.expect.map((predicate) => resolvePredicate(predicate, ctx))
971
877
  };
972
878
  }
973
- function readPreconditionDepMappingKeys(handle) {
974
- return readPreconditionDepMapping(handle).map(([, depName]) => depName);
975
- }
976
- function readDescriptionOf(handle) {
977
- return handle.description;
978
- }
979
- function readDescriptionOfObserver(handle) {
980
- return handle.description;
879
+ function resolveAction(action, ctx) {
880
+ if (action.kind === "goto") {
881
+ return { ...action, url: resolveString(action.url, ctx) };
882
+ }
883
+ if (action.kind === "fill" || action.kind === "select") {
884
+ return {
885
+ ...action,
886
+ locator: resolveLocator(action.locator, ctx),
887
+ value: resolveValue(action.value, ctx)
888
+ };
889
+ }
890
+ if (action.kind === "press") {
891
+ return {
892
+ ...action,
893
+ locator: action.locator == null ? void 0 : resolveLocator(action.locator, ctx)
894
+ };
895
+ }
896
+ if (action.kind === "upload") {
897
+ return { ...action, locator: resolveLocator(action.locator, ctx) };
898
+ }
899
+ return { ...action, locator: resolveLocator(action.locator, ctx) };
981
900
  }
982
- function readBudgetOf(handle) {
983
- return handle.budget;
901
+ function resolveString(value2, ctx) {
902
+ if (isBinding(value2)) {
903
+ return resolveBinding(value2, ctx);
904
+ }
905
+ if (isTemplate(value2)) {
906
+ return resolveTemplate(value2, ctx);
907
+ }
908
+ return value2;
984
909
  }
985
- async function executePreconditions({
986
- defsByName,
987
- items,
988
- options
989
- }) {
990
- const defaultDomain = deriveDefaultDomain(options?.appUrl);
991
- const initial = items.map((item) => {
992
- const cookiesRef = [];
910
+ function resolveLocator(locator, ctx) {
911
+ if (locator.by === "inside") {
993
912
  return {
994
- cookies: cookiesRef,
995
- cookiesRef,
996
- ctx: createSetupContext({ cookies: cookiesRef, defaultDomain, runId: item.runId }),
997
- data: {},
998
- executed: [],
999
- failure: void 0,
1000
- names: item.names,
1001
- runId: item.runId
913
+ by: "inside",
914
+ scope: resolveLocator(locator.scope, ctx),
915
+ target: resolveLocator(locator.target, ctx)
1002
916
  };
1003
- });
1004
- const order = topoOrder(defsByName, items);
1005
- const orderError = order.error;
1006
- if (orderError != null) {
1007
- return initial.map((s) => failResult(s, orderError));
1008
- }
1009
- const sequence = order.names ?? [];
1010
- let states = initial;
1011
- let i = 0;
1012
- while (i < sequence.length) {
1013
- const name = sequence[i];
1014
- if (name != null) {
1015
- states = await executeStep({ defsByName, name, states });
917
+ }
918
+ if (locator.by === "role") {
919
+ return locator.name == null ? locator : { ...locator, name: resolveString(locator.name, ctx) };
920
+ }
921
+ return { ...locator, value: resolveString(locator.value, ctx) };
922
+ }
923
+ function resolvePredicate(predicate, ctx) {
924
+ switch (predicate.kind) {
925
+ case "visible":
926
+ case "disabled":
927
+ case "enabled":
928
+ case "focused": {
929
+ return { ...predicate, locator: resolveLocator(predicate.locator, ctx) };
1016
930
  }
1017
- i += 1;
1018
- }
1019
- return states.map(
1020
- (s) => s.failure == null ? {
1021
- cookies: s.cookiesRef,
1022
- data: s.data,
1023
- error: void 0,
1024
- executed: s.executed,
1025
- runId: s.runId,
1026
- success: true
1027
- } : failResult(s, s.failure)
1028
- );
1029
- }
1030
- function topoOrder(defsByName, items) {
1031
- const seen = /* @__PURE__ */ new Set();
1032
- const order = [];
1033
- const visiting = /* @__PURE__ */ new Set();
1034
- const visitDeps = (deps) => {
1035
- const found = deps.find((d) => visit(d) != null);
1036
- return found == null ? void 0 : visit(found);
1037
- };
1038
- const visit = (name) => {
1039
- if (seen.has(name)) {
1040
- return void 0;
931
+ case "value":
932
+ case "text": {
933
+ return {
934
+ ...predicate,
935
+ locator: resolveLocator(predicate.locator, ctx),
936
+ value: resolveString(predicate.value, ctx)
937
+ };
1041
938
  }
1042
- if (visiting.has(name)) {
1043
- return `Cycle detected at precondition "${name}"`;
939
+ case "singleton": {
940
+ return {
941
+ ...predicate,
942
+ assertion: { ...predicate.assertion, value: resolveValue(predicate.assertion.value, ctx) }
943
+ };
1044
944
  }
1045
- const def = defsByName.get(name);
1046
- if (def == null) {
1047
- return `Unknown precondition: "${name}"`;
945
+ case "browser": {
946
+ return { ...predicate, value: resolveString(predicate.value, ctx) };
1048
947
  }
1049
- visiting.add(name);
1050
- const deps = def.depMapping.map(([, depName]) => depName);
1051
- const cycleError = visitDeps(deps);
1052
- visiting.delete(name);
1053
- if (cycleError != null) {
1054
- return cycleError;
948
+ case "state": {
949
+ return {
950
+ ...predicate,
951
+ assertion: resolveAssertion(predicate.assertion, ctx),
952
+ key: whereMap(predicate.key, ctx)
953
+ };
1055
954
  }
1056
- seen.add(name);
1057
- order.push(name);
1058
- return void 0;
1059
- };
1060
- let err;
1061
- let k = 0;
1062
- const allNames = items.flatMap((item) => item.names);
1063
- while (k < allNames.length && err == null) {
1064
- const n = allNames[k];
1065
- if (n != null) {
1066
- err = visit(n);
955
+ case "not": {
956
+ return { ...predicate, predicate: resolvePredicate(predicate.predicate, ctx) };
1067
957
  }
1068
- k += 1;
1069
- }
1070
- if (err != null) {
1071
- return { error: err, names: void 0 };
1072
- }
1073
- return { error: void 0, names: order };
1074
- }
1075
- async function executeStep({
1076
- defsByName,
1077
- name,
1078
- states
1079
- }) {
1080
- const isActive = (s) => s.failure == null && s.names.includes(name);
1081
- const active = states.filter((s) => isActive(s));
1082
- if (active.length === 0) {
1083
- return states;
1084
- }
1085
- const def = defsByName.get(name);
1086
- if (def == null) {
1087
- return states.map(
1088
- (s) => isActive(s) ? { ...s, failure: `Unknown precondition: "${name}"` } : s
1089
- );
1090
- }
1091
- if (!def.implemented) {
1092
- return states.map(
1093
- (s) => isActive(s) ? { ...s, failure: `Precondition "${name}" is not implemented` } : s
1094
- );
1095
- }
1096
- const stepResult = await runSetup({ active, def, name });
1097
- return states.map((s) => isActive(s) ? mergeStepResult(s, name, stepResult) : s);
1098
- }
1099
- async function runSetup({ active, def, name }) {
1100
- const itemsForCall = active.map((s) => ({ ctx: s.ctx, deps: s.data }));
1101
- try {
1102
- const results = await def.setup(itemsForCall);
1103
- if (results.length !== active.length) {
958
+ case "when": {
1104
959
  return {
1105
- batchError: `Precondition "${name}" returned ${String(results.length)} results for ${String(active.length)} items`,
1106
- perItem: /* @__PURE__ */ new Map()
960
+ ...predicate,
961
+ condition: resolvePredicate(predicate.condition, ctx),
962
+ consequence: resolvePredicate(predicate.consequence, ctx),
963
+ otherwise: predicate.otherwise == null ? void 0 : resolvePredicate(predicate.otherwise, ctx)
1107
964
  };
1108
965
  }
1109
- const entries = active.map((s, idx) => {
1110
- const r = results[idx];
1111
- return r == null ? void 0 : [s.runId, r];
1112
- });
1113
- const valid = entries.filter(
1114
- (e) => e != null
1115
- );
1116
- if (valid.length !== active.length) {
966
+ case "and": {
1117
967
  return {
1118
- batchError: `Precondition "${name}" returned a null result for at least one item`,
1119
- perItem: /* @__PURE__ */ new Map()
968
+ ...predicate,
969
+ predicates: predicate.predicates.map((p) => resolvePredicate(p, ctx))
1120
970
  };
1121
971
  }
1122
- return { batchError: void 0, perItem: new Map(valid) };
1123
- } catch (error) {
1124
- return {
1125
- batchError: error instanceof Error ? error.message : String(error),
1126
- perItem: /* @__PURE__ */ new Map()
1127
- };
972
+ case "count": {
973
+ return predicate;
974
+ }
1128
975
  }
1129
976
  }
1130
- function mergeStepResult(s, name, result) {
1131
- if (result.batchError != null) {
1132
- return { ...s, failure: result.batchError };
977
+ function resolveAssertion(assertion, ctx) {
978
+ if (assertion.kind === "deleted") {
979
+ return assertion;
980
+ }
981
+ const descriptor = ctx.captures.get(assertion);
982
+ if (descriptor == null) {
983
+ throw new Error("internal: capture assertion was not registered");
1133
984
  }
1134
- const data = result.perItem.get(s.runId);
1135
- if (data == null) {
1136
- return { ...s, failure: `Precondition "${name}" produced no result for run "${s.runId}"` };
985
+ const as = aliasFor(ctx, descriptor);
986
+ return assertion.kind === "created" ? { as, kind: "created", props: setMap(assertion.props, ctx) } : { as, kind: "updated", props: updateMap(assertion.props, ctx) };
987
+ }
988
+ function updateMap(map, ctx) {
989
+ return Object.fromEntries(
990
+ Object.entries(map).map(([key2, value2]) => [
991
+ key2,
992
+ isChanged(value2) ? value2 : resolveValue(value2, ctx)
993
+ ])
994
+ );
995
+ }
996
+ function whereMap(map, ctx) {
997
+ return Object.fromEntries(
998
+ Object.entries(map).map(([key2, value2]) => [
999
+ key2,
1000
+ resolveWhere(value2, ctx)
1001
+ ])
1002
+ );
1003
+ }
1004
+ function resolveWhere(value2, ctx) {
1005
+ return isWithin2(value2) ? { ...value2, selection: { ...value2.selection, where: whereMap(value2.selection.where, ctx) } } : resolveValue(value2, ctx);
1006
+ }
1007
+
1008
+ // src/test.ts
1009
+ function test(intent, fn) {
1010
+ const sourcePath = captureSourcePath();
1011
+ if (fn == null) {
1012
+ return { spec: stubSpec(intent, sourcePath) };
1137
1013
  }
1014
+ const final = finalize(fn());
1138
1015
  return {
1139
- ...s,
1140
- data: { ...s.data, [name]: data },
1141
- executed: [...s.executed, name]
1016
+ spec: {
1017
+ absent: final.absent,
1018
+ exclusive: final.exclusive,
1019
+ intent,
1020
+ maybe: final.maybe,
1021
+ name: slugify(intent),
1022
+ params: final.params,
1023
+ singletons: final.singletons,
1024
+ sourcePath,
1025
+ steps: final.steps,
1026
+ stub: false,
1027
+ world: final.world
1028
+ }
1142
1029
  };
1143
1030
  }
1144
- function failResult(s, error) {
1031
+ function stubSpec(intent, sourcePath) {
1145
1032
  return {
1146
- cookies: s.cookies,
1147
- data: s.data,
1148
- error,
1149
- executed: s.executed,
1150
- runId: s.runId,
1151
- success: false
1033
+ absent: [],
1034
+ exclusive: [],
1035
+ intent,
1036
+ maybe: [],
1037
+ name: slugify(intent),
1038
+ params: {},
1039
+ singletons: {},
1040
+ sourcePath,
1041
+ steps: [],
1042
+ stub: true,
1043
+ world: []
1152
1044
  };
1153
1045
  }
1154
- async function executeObserver(observersByName, name, params) {
1155
- const def = observersByName.get(name);
1156
- if (def == null) {
1157
- return { error: `Unknown observer: "${name}"`, outcome: void 0, success: false };
1158
- }
1159
- if (!def.implemented) {
1160
- return { error: `Observer "${name}" is not implemented`, outcome: void 0, success: false };
1046
+ var TESTS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]tests[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
1047
+ function captureSourcePath() {
1048
+ const stack = new Error("capture").stack;
1049
+ if (stack == null) {
1050
+ return void 0;
1161
1051
  }
1162
- const ctx = createObserverContext(crypto.randomUUID().slice(0, 12));
1163
- try {
1164
- const outcome = await def.run(ctx, params);
1165
- return { error: void 0, outcome, success: true };
1166
- } catch (error) {
1167
- const message = error instanceof Error ? error.message : String(error);
1168
- return { error: void 0, outcome: createFailOutcome(message), success: true };
1052
+ const match = stack.split("\n").map((line) => TESTS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
1053
+ const captured = match?.[1];
1054
+ return captured == null ? void 0 : captured.replaceAll("\\", "/");
1055
+ }
1056
+ function slugify(intent) {
1057
+ const slug = intent.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim().split(" ").join("-");
1058
+ if (slug.length === 0) {
1059
+ throw new Error(`test intent "${intent}" slugifies to an empty string`);
1169
1060
  }
1061
+ return slug;
1170
1062
  }
1171
- function createObserverContext(runId) {
1063
+
1064
+ // src/params.ts
1065
+ function arbitrary(field2, example) {
1066
+ const token = {
1067
+ base: `${field2.entity}_${field2.field}`,
1068
+ example,
1069
+ valueSpace: field2.valueSpaceName
1070
+ };
1071
+ return paramBinding(token);
1072
+ }
1073
+
1074
+ // src/engine.ts
1075
+ import { err, ok, okAsync, Result, ResultAsync } from "neverthrow";
1076
+ function createEngine(ripplo, impls, teardown) {
1077
+ const entities = new Map(Object.entries(impls.entities));
1078
+ const singletons = new Map(
1079
+ Object.entries(impls.singletons).map(([name, impl]) => [
1080
+ name,
1081
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
1082
+ impl
1083
+ ])
1084
+ );
1085
+ assertDeclared(ripplo, [...entities.keys()], [...singletons.keys()]);
1086
+ const entityImpl = (name) => lookup(entities.get(name), name);
1087
+ const singletonImpl = (name) => lookup(singletons.get(name), name);
1172
1088
  return {
1173
- runId,
1174
- fail: (reason) => createFailOutcome(reason),
1175
- pass: () => createPassOutcome(),
1176
- retry: (reason) => createRetryOutcome(reason)
1089
+ read: (request, runId) => ResultAsync.combine([
1090
+ ResultAsync.combine(
1091
+ request.entities.map((name) => readEntity(entityImpl(name), name, runId))
1092
+ ),
1093
+ ResultAsync.combine(
1094
+ request.singletons.map((name) => readSingleton(singletonImpl(name), name, runId))
1095
+ )
1096
+ ]).map(([entityPairs, singletonPairs]) => ({
1097
+ entities: Object.fromEntries(entityPairs),
1098
+ singletons: Object.fromEntries(singletonPairs)
1099
+ })),
1100
+ seed: (request, runId) => ResultAsync.fromPromise(teardown(runId), implFailure).andThen(() => seedSingletons(request.singletons, singletonImpl, runId)).andThen(() => seedSetups(request.entities, entityImpl, runId)),
1101
+ teardown: (runId) => ResultAsync.fromPromise(teardown(runId), implFailure)
1177
1102
  };
1178
1103
  }
1179
- async function teardown({
1180
- defsByName,
1181
- items
1182
- }) {
1183
- const errors = /* @__PURE__ */ new Map();
1184
- const allNamesUnique = [...new Set(items.flatMap((i2) => i2.names))];
1185
- const order = topoOrder(
1186
- defsByName,
1187
- allNamesUnique.map((n) => ({ names: [n], runId: "" }))
1104
+ function assertDeclared(ripplo, entityNames, singletonNames) {
1105
+ const backendEntities = new Set(
1106
+ ripplo.entities.filter((e) => e.schema.source === "backend").map((e) => e.name)
1107
+ );
1108
+ const backendSingletons = new Set(
1109
+ ripplo.singletons.filter((s) => s.schema.source === "backend").map((s) => s.name)
1188
1110
  );
1189
- const reversed = (order.names ?? []).toReversed();
1190
- let i = 0;
1191
- while (i < reversed.length) {
1192
- const name = reversed[i];
1193
- if (name != null) {
1194
- await teardownStep({ defsByName, errors, items, name });
1111
+ entityNames.forEach((name) => {
1112
+ if (!backendEntities.has(name)) {
1113
+ throw new Error(`engine impl "${name}" has no matching backend entity`);
1114
+ }
1115
+ });
1116
+ singletonNames.forEach((name) => {
1117
+ if (!backendSingletons.has(name)) {
1118
+ throw new Error(`engine impl "${name}" has no matching backend singleton`);
1195
1119
  }
1196
- i += 1;
1197
- }
1198
- return items.map((it) => {
1199
- const err = errors.get(it.runId);
1200
- return { error: err, runId: it.runId, success: err == null };
1201
1120
  });
1202
1121
  }
1203
- async function teardownStep({
1204
- defsByName,
1205
- errors,
1206
- items,
1207
- name
1208
- }) {
1209
- const def = defsByName.get(name);
1210
- if (def?.teardown == null) {
1211
- return;
1122
+ function lookup(impl, name) {
1123
+ return impl == null ? err({ message: `no engine impl for "${name}"` }) : ok(impl);
1124
+ }
1125
+ function readEntity(impl, name, runId) {
1126
+ return impl.asyncAndThen(
1127
+ (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((rows) => [
1128
+ name,
1129
+ [...rows]
1130
+ ])
1131
+ );
1132
+ }
1133
+ function implFailure(error) {
1134
+ return { message: error instanceof Error ? error.message : String(error) };
1135
+ }
1136
+ function readSingleton(impl, name, runId) {
1137
+ return impl.asyncAndThen(
1138
+ (i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((value2) => [
1139
+ name,
1140
+ value2
1141
+ ])
1142
+ );
1143
+ }
1144
+ function seedSingletons(values, singletonImpl, runId) {
1145
+ return ResultAsync.combine(
1146
+ Object.entries(values).map(
1147
+ ([name, value2]) => singletonImpl(name).asyncAndThen(
1148
+ (impl) => ResultAsync.fromPromise(impl.seed({ runId, value: value2 }), implFailure)
1149
+ )
1150
+ )
1151
+ );
1152
+ }
1153
+ function seedSetups(specs, entityImpl, runId) {
1154
+ const waves = dependencyWaves(specs);
1155
+ return waves.reduce((accR, wave) => accR.andThen((acc) => seedWave(wave, entityImpl, acc, runId)), okAsync({ env: /* @__PURE__ */ new Map(), rows: [] })).map((fold) => orderRows(specs, fold.rows));
1156
+ }
1157
+ function dependencyWaves(specs) {
1158
+ return buildWaves([], specs);
1159
+ }
1160
+ function buildWaves(done, remaining) {
1161
+ if (remaining.length === 0) {
1162
+ return done;
1212
1163
  }
1213
- const active = items.filter((i) => i.names.includes(name));
1214
- if (active.length === 0) {
1215
- return;
1164
+ const seeded = new Set(done.flat().map((spec) => spec.as));
1165
+ const ready = remaining.filter((spec) => refAliases(spec).every((alias) => seeded.has(alias)));
1166
+ if (ready.length === 0) {
1167
+ return [...done, remaining];
1216
1168
  }
1217
- const teardownItems = active.map((it) => ({
1218
- ctx: { data: it.data[name] ?? {} }
1219
- }));
1220
- try {
1221
- await def.teardown(teardownItems);
1222
- } catch (error) {
1223
- const message = error instanceof Error ? error.message : String(error);
1224
- active.forEach((it) => {
1225
- if (!errors.has(it.runId)) {
1226
- errors.set(it.runId, message);
1227
- }
1228
- });
1169
+ return buildWaves(
1170
+ [...done, ready],
1171
+ remaining.filter((spec) => !ready.includes(spec))
1172
+ );
1173
+ }
1174
+ function refAliases(spec) {
1175
+ return Object.values(spec.fields).flatMap((value2) => {
1176
+ if (value2 === null || typeof value2 !== "object" || !("ref" in value2)) {
1177
+ return [];
1178
+ }
1179
+ const lastDot = value2.ref.lastIndexOf(".");
1180
+ return lastDot === -1 ? [] : [value2.ref.slice(0, lastDot)];
1181
+ });
1182
+ }
1183
+ function seedWave(wave, entityImpl, acc, runId) {
1184
+ return ResultAsync.combine(
1185
+ wave.map((spec) => seedSetup(entityImpl(spec.entity), acc, spec, runId))
1186
+ ).map((folds) => mergeFolds(acc, folds));
1187
+ }
1188
+ function seedSetup(impl, acc, spec, runId) {
1189
+ return resolveFields(spec.fields, acc.env).asyncAndThen((fields) => runSeed(impl, fields, runId)).map(({ row: row2, session }) => foldRow(acc, spec, row2, session));
1190
+ }
1191
+ function resolveFields(fields, env) {
1192
+ return Result.combine(
1193
+ Object.entries(fields).map(
1194
+ ([field2, value2]) => resolveValue2(value2, env).map((cell2) => [field2, cell2])
1195
+ )
1196
+ ).map((entries) => Object.fromEntries(entries));
1197
+ }
1198
+ function resolveValue2(value2, env) {
1199
+ if (value2 === null || typeof value2 !== "object") {
1200
+ return ok(value2);
1229
1201
  }
1202
+ if ("ref" in value2) {
1203
+ return env.has(value2.ref) ? ok(env.get(value2.ref) ?? null) : err({ message: `setup ref "${value2.ref}" was not produced by an earlier entity` });
1204
+ }
1205
+ throw new Error("internal: a setup value resolved to an unresolved template");
1206
+ }
1207
+ function runSeed(impl, fields, runId) {
1208
+ return impl.asyncAndThen((i) => ResultAsync.fromPromise(i.seed({ fields, runId }), implFailure));
1209
+ }
1210
+ function foldRow(acc, spec, row2, session) {
1211
+ const next = Object.entries(row2).map(([field2, value2]) => [
1212
+ `${spec.as}.${field2}`,
1213
+ value2
1214
+ ]);
1215
+ return {
1216
+ env: new Map([...acc.env, ...next]),
1217
+ rows: [...acc.rows, { as: spec.as, row: row2, session }]
1218
+ };
1230
1219
  }
1231
- function createSetupContext({
1232
- cookies,
1233
- defaultDomain,
1234
- runId
1220
+ function mergeFolds(base, folds) {
1221
+ return {
1222
+ env: new Map([...base.env, ...folds.flatMap((fold) => [...fold.env])]),
1223
+ rows: [...base.rows, ...folds.flatMap((fold) => fold.rows.slice(base.rows.length))]
1224
+ };
1225
+ }
1226
+ function orderRows(specs, rows) {
1227
+ const byAs = new Map(rows.map((row2) => [row2.as, row2]));
1228
+ return specs.flatMap((spec) => {
1229
+ const row2 = byAs.get(spec.as);
1230
+ return row2 == null ? [] : [row2];
1231
+ });
1232
+ }
1233
+
1234
+ // src/client-engine.ts
1235
+ import { z as z8 } from "zod";
1236
+
1237
+ // ../spec/src/leaves.ts
1238
+ import { z } from "zod";
1239
+ var budgetSchema = z.enum(["fast", "slow", "async"]);
1240
+ var valueRefSchema = z.object({ ref: z.string().min(1) });
1241
+ var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
1242
+ var templateSchema = z.object({
1243
+ template: z.array(z.union([z.string(), valueRefSchema])).min(1)
1244
+ });
1245
+ var setValueSchema = z.union([valueRefSchema, primitiveSchema, templateSchema, z.null()]);
1246
+ var changedSchema = z.object({ kind: z.literal("changed") });
1247
+ var updateValueSchema = z.union([setValueSchema, changedSchema]);
1248
+ var stringValueSchema = z.union([z.string(), valueRefSchema, templateSchema]);
1249
+ var roleLocatorSchema = z.object({
1250
+ by: z.literal("role"),
1251
+ name: z.union([stringValueSchema, z.undefined()]).optional().transform((value2) => value2),
1252
+ role: z.string().min(1)
1253
+ });
1254
+ var testIdLocatorSchema = z.object({ by: z.literal("testId"), value: stringValueSchema });
1255
+ var insideLocatorSchema = z.object({
1256
+ by: z.literal("inside"),
1257
+ scope: z.lazy(() => locatorSchema),
1258
+ target: z.lazy(() => locatorSchema)
1259
+ });
1260
+ var locatorSchema = z.discriminatedUnion("by", [
1261
+ roleLocatorSchema,
1262
+ testIdLocatorSchema,
1263
+ insideLocatorSchema
1264
+ ]);
1265
+ var primitiveTypeSchema = z.enum(["string", "number", "boolean"]);
1266
+ var consistencyClassSchema = z.enum(["strict", "eventual"]);
1267
+ var stringConstraintsSchema = z.object({
1268
+ kind: z.literal("string"),
1269
+ maxLength: z.number().int().positive().optional(),
1270
+ minLength: z.number().int().nonnegative().optional(),
1271
+ pattern: z.string().optional()
1272
+ });
1273
+ var numberConstraintsSchema = z.object({
1274
+ kind: z.literal("number"),
1275
+ max: z.number().int().optional(),
1276
+ min: z.number().int().optional()
1277
+ });
1278
+ var datetimeConstraintsSchema = z.object({
1279
+ kind: z.literal("datetime"),
1280
+ maxOffsetDays: z.number().int(),
1281
+ minOffsetDays: z.number().int()
1282
+ });
1283
+ var constraintsSchema = z.discriminatedUnion("kind", [
1284
+ stringConstraintsSchema,
1285
+ numberConstraintsSchema,
1286
+ datetimeConstraintsSchema
1287
+ ]);
1288
+ var generatorSchema = z.enum([
1289
+ "company.name",
1290
+ "date.iso",
1291
+ "internet.email",
1292
+ "internet.url",
1293
+ "person.fullName",
1294
+ "lorem.slug",
1295
+ "lorem.word"
1296
+ ]);
1297
+ var valueSpaceSchema = z.object({
1298
+ constraints: constraintsSchema.optional(),
1299
+ generator: generatorSchema,
1300
+ name: z.string().min(1),
1301
+ type: primitiveTypeSchema,
1302
+ values: z.array(primitiveSchema).min(1).optional()
1303
+ });
1304
+ var propSpecSchema = z.object({
1305
+ consistency: consistencyClassSchema.default("strict"),
1306
+ optional: z.boolean(),
1307
+ stable: z.boolean(),
1308
+ type: primitiveTypeSchema,
1309
+ valueSpace: z.string().min(1).optional()
1310
+ });
1311
+ var sourceSchema = z.enum(["backend", "client"]);
1312
+ var entitySchemaSchema = z.object({
1313
+ description: z.string().optional(),
1314
+ identity: z.array(z.string().min(1)).min(1),
1315
+ identityKind: z.enum(["surrogate", "natural"]),
1316
+ name: z.string().min(1),
1317
+ props: z.record(z.string().min(1), propSpecSchema),
1318
+ source: sourceSchema.default("backend")
1319
+ });
1320
+ var singletonSchemaSchema = z.object({
1321
+ consistency: consistencyClassSchema.default("strict"),
1322
+ default: primitiveSchema,
1323
+ description: z.string().optional(),
1324
+ name: z.string().min(1),
1325
+ source: sourceSchema.default("backend"),
1326
+ type: primitiveTypeSchema,
1327
+ valueSpace: z.string().min(1).optional()
1328
+ });
1329
+ var browserSingletonSchema = z.enum(["url", "title", "viewport"]);
1330
+
1331
+ // ../spec/src/predicate.ts
1332
+ import { z as z2 } from "zod";
1333
+ var stateAssertionSchema = z2.discriminatedUnion("kind", [
1334
+ z2.object({
1335
+ as: z2.string().min(1),
1336
+ kind: z2.literal("created"),
1337
+ props: z2.record(z2.string().min(1), setValueSchema)
1338
+ }),
1339
+ z2.object({
1340
+ as: z2.string().min(1),
1341
+ kind: z2.literal("updated"),
1342
+ props: z2.record(z2.string().min(1), updateValueSchema)
1343
+ }),
1344
+ z2.object({ kind: z2.literal("deleted") })
1345
+ ]);
1346
+ var singletonAssertionSchema = z2.object({ kind: z2.literal("is"), value: setValueSchema });
1347
+ var whereValueSchema = z2.lazy(
1348
+ () => z2.union([setValueSchema, withinSchema])
1349
+ );
1350
+ var selectionSchema = z2.object({
1351
+ entity: z2.string().min(1),
1352
+ where: z2.record(z2.string().min(1), whereValueSchema)
1353
+ });
1354
+ var withinSchema = z2.object({
1355
+ field: z2.string().min(1),
1356
+ kind: z2.literal("within"),
1357
+ selection: selectionSchema
1358
+ });
1359
+ var wait = z2.union([budgetSchema, z2.undefined()]).optional().transform((value2) => value2);
1360
+ var predicateSchema = z2.lazy(
1361
+ () => z2.discriminatedUnion("kind", [
1362
+ z2.object({ kind: z2.literal("visible"), locator: locatorSchema, wait }),
1363
+ z2.object({ kind: z2.literal("disabled"), locator: locatorSchema, wait }),
1364
+ z2.object({ kind: z2.literal("enabled"), locator: locatorSchema, wait }),
1365
+ z2.object({ kind: z2.literal("focused"), locator: locatorSchema, wait }),
1366
+ z2.object({ kind: z2.literal("value"), locator: locatorSchema, value: stringValueSchema, wait }),
1367
+ z2.object({ kind: z2.literal("text"), locator: locatorSchema, value: stringValueSchema, wait }),
1368
+ z2.object({
1369
+ assertion: singletonAssertionSchema,
1370
+ kind: z2.literal("singleton"),
1371
+ singleton: z2.string().min(1),
1372
+ wait
1373
+ }),
1374
+ z2.object({
1375
+ kind: z2.literal("browser"),
1376
+ name: browserSingletonSchema,
1377
+ value: stringValueSchema,
1378
+ wait
1379
+ }),
1380
+ z2.object({
1381
+ assertion: stateAssertionSchema,
1382
+ entity: z2.string().min(1),
1383
+ key: z2.record(z2.string().min(1), whereValueSchema),
1384
+ kind: z2.literal("state"),
1385
+ wait
1386
+ }),
1387
+ z2.object({ kind: z2.literal("not"), predicate: predicateSchema }),
1388
+ z2.object({ kind: z2.literal("and"), predicates: z2.array(predicateSchema) }),
1389
+ z2.object({
1390
+ entity: z2.string().min(1),
1391
+ kind: z2.literal("count"),
1392
+ value: z2.number().int().nonnegative()
1393
+ }),
1394
+ z2.object({
1395
+ condition: predicateSchema,
1396
+ consequence: predicateSchema,
1397
+ kind: z2.literal("when"),
1398
+ otherwise: z2.union([predicateSchema, z2.undefined()]).optional().transform((value2) => value2)
1399
+ })
1400
+ ])
1401
+ );
1402
+
1403
+ // ../spec/src/codec.ts
1404
+ import { z as z3 } from "zod";
1405
+ var envelopeSchema = z3.object({
1406
+ __codec: z3.string().min(1),
1407
+ data: z3.unknown(),
1408
+ version: z3.number().int().positive()
1409
+ });
1410
+ var CodecVersionError = class extends Error {
1411
+ codec;
1412
+ currentVersion;
1413
+ gotVersion;
1414
+ constructor(params) {
1415
+ super(
1416
+ `Unsupported ${params.codec} version ${String(params.gotVersion)} (current ${String(params.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`
1417
+ );
1418
+ this.name = "CodecVersionError";
1419
+ this.codec = params.codec;
1420
+ this.currentVersion = params.currentVersion;
1421
+ this.gotVersion = params.gotVersion;
1422
+ }
1423
+ };
1424
+ var CodecMismatchError = class extends Error {
1425
+ constructor(params) {
1426
+ super(`Codec mismatch: expected "${params.expected}", got "${params.got}"`);
1427
+ this.name = "CodecMismatchError";
1428
+ }
1429
+ };
1430
+ function defineCodec({
1431
+ name,
1432
+ schema
1235
1433
  }) {
1236
- let callCounter = 0;
1237
- const nextSuffix = () => {
1238
- callCounter += 1;
1239
- return `${runId}-${String(callCounter)}`;
1434
+ return {
1435
+ currentVersion: 1,
1436
+ name,
1437
+ decode: (raw) => decode({ name, raw, schema }),
1438
+ encode: (value2) => ({
1439
+ __codec: name,
1440
+ data: value2,
1441
+ version: 1
1442
+ })
1240
1443
  };
1444
+ }
1445
+ function decode({
1446
+ name,
1447
+ raw,
1448
+ schema
1449
+ }) {
1450
+ const envelope = envelopeSchema.parse(raw);
1451
+ if (envelope.__codec !== name) {
1452
+ throw new CodecMismatchError({ expected: name, got: envelope.__codec });
1453
+ }
1454
+ if (envelope.version !== 1) {
1455
+ throw new CodecVersionError({ codec: name, currentVersion: 1, gotVersion: envelope.version });
1456
+ }
1457
+ return schema.parse(envelope.data);
1458
+ }
1459
+
1460
+ // ../spec/src/client-channel.ts
1461
+ var CLIENT_MOUNT_KEY = "__ripplo__";
1462
+ var CLIENT_SEED_KEY = "__ripplo_seed__";
1463
+
1464
+ // ../spec/src/lockfile.ts
1465
+ import { z as z4 } from "zod";
1466
+ var actionSchema = z4.discriminatedUnion("kind", [
1467
+ z4.object({ kind: z4.literal("goto"), url: stringValueSchema }),
1468
+ z4.object({ kind: z4.literal("fill"), locator: locatorSchema, value: setValueSchema }),
1469
+ z4.object({ kind: z4.literal("clear"), locator: locatorSchema }),
1470
+ z4.object({ kind: z4.literal("click"), locator: locatorSchema }),
1471
+ z4.object({ kind: z4.literal("dblclick"), locator: locatorSchema }),
1472
+ z4.object({ kind: z4.literal("select"), locator: locatorSchema, value: setValueSchema }),
1473
+ z4.object({ kind: z4.literal("check"), locator: locatorSchema }),
1474
+ z4.object({ kind: z4.literal("uncheck"), locator: locatorSchema }),
1475
+ z4.object({ kind: z4.literal("hover"), locator: locatorSchema }),
1476
+ z4.object({
1477
+ files: z4.array(z4.string().min(1)).min(1),
1478
+ kind: z4.literal("upload"),
1479
+ locator: locatorSchema
1480
+ }),
1481
+ z4.object({ key: z4.string().min(1), kind: z4.literal("press"), locator: locatorSchema.optional() })
1482
+ ]);
1483
+ var stepSchema = z4.object({
1484
+ action: actionSchema,
1485
+ expect: z4.array(predicateSchema).default([])
1486
+ });
1487
+ var paramSchema = z4.object({
1488
+ example: primitiveSchema.optional(),
1489
+ valueSpace: z4.string().min(1)
1490
+ });
1491
+ var setupSchema = z4.object({
1492
+ as: z4.string().min(1),
1493
+ entity: z4.string().min(1),
1494
+ set: z4.record(z4.string().min(1), setValueSchema)
1495
+ });
1496
+ var absenceSchema = z4.object({
1497
+ entity: z4.string().min(1),
1498
+ where: z4.record(z4.string().min(1), setValueSchema)
1499
+ });
1500
+ var testSchema = z4.object({
1501
+ absent: z4.array(absenceSchema).default([]),
1502
+ exclusive: z4.array(z4.string().min(1)).default([]),
1503
+ intent: z4.string().min(1),
1504
+ maybe: z4.array(setupSchema).default([]),
1505
+ name: z4.string().min(1),
1506
+ params: z4.record(z4.string().min(1), paramSchema),
1507
+ singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
1508
+ sourcePath: z4.string().min(1).optional(),
1509
+ steps: z4.array(stepSchema).default([]),
1510
+ stub: z4.boolean().default(false),
1511
+ world: z4.array(setupSchema).default([])
1512
+ });
1513
+ var fixtureEntrySchema = z4.object({
1514
+ sha256: z4.string().regex(/^[0-9a-f]{64}$/u),
1515
+ size: z4.number().int().nonnegative()
1516
+ });
1517
+ var lockfileSchema = z4.object({
1518
+ entities: z4.array(entitySchemaSchema),
1519
+ fixtures: z4.record(z4.string().min(1), fixtureEntrySchema).default({}),
1520
+ singletons: z4.array(singletonSchemaSchema).default([]),
1521
+ tests: z4.array(testSchema),
1522
+ valueSpaces: z4.array(valueSpaceSchema)
1523
+ });
1524
+ var lockfileCodec = defineCodec({ name: "ripplo-lockfile", schema: lockfileSchema });
1525
+
1526
+ // ../spec/src/sync-payload.ts
1527
+ import { z as z5 } from "zod";
1528
+ var stepDescriptorSchema = z5.object({
1529
+ index: z5.number().int().nonnegative(),
1530
+ kind: z5.string(),
1531
+ target: z5.string(),
1532
+ value: z5.string()
1533
+ });
1534
+ var workflowSpecSchema = z5.object({
1535
+ steps: z5.array(stepDescriptorSchema),
1536
+ stub: z5.boolean().default(false)
1537
+ });
1538
+
1539
+ // ../spec/src/session.ts
1540
+ import { z as z6 } from "zod";
1541
+ var sameSiteSchema = z6.enum(["Strict", "Lax", "None"]);
1542
+ var cookieSchema = z6.object({
1543
+ domain: z6.string().min(1),
1544
+ expires: z6.number(),
1545
+ httpOnly: z6.boolean(),
1546
+ name: z6.string().min(1),
1547
+ path: z6.string().min(1),
1548
+ sameSite: sameSiteSchema,
1549
+ secure: z6.boolean(),
1550
+ value: z6.string()
1551
+ });
1552
+ var originSchema = z6.object({
1553
+ localStorage: z6.array(z6.object({ name: z6.string().min(1), value: z6.string() })),
1554
+ origin: z6.string().min(1)
1555
+ });
1556
+ var sessionSchema = z6.object({
1557
+ cookies: z6.array(cookieSchema),
1558
+ headers: z6.record(z6.string().min(1), z6.string()).optional(),
1559
+ origins: z6.array(originSchema)
1560
+ });
1561
+
1562
+ // ../spec/src/engine.ts
1563
+ import { z as z7 } from "zod";
1564
+ var cellSchema = z7.union([primitiveSchema, z7.null()]);
1565
+ var rowSchema = z7.record(z7.string().min(1), cellSchema);
1566
+ var setupSpecSchema = z7.object({
1567
+ as: z7.string().min(1),
1568
+ entity: z7.string().min(1),
1569
+ fields: z7.record(z7.string().min(1), setValueSchema)
1570
+ });
1571
+ var setupRequestSchema = z7.object({
1572
+ entities: z7.array(setupSpecSchema),
1573
+ runId: z7.string().min(1),
1574
+ singletons: z7.record(z7.string().min(1), cellSchema).default({})
1575
+ });
1576
+ var setupRowSchema = z7.object({
1577
+ as: z7.string().min(1),
1578
+ row: rowSchema,
1579
+ session: sessionSchema.optional()
1580
+ });
1581
+ var setupResponseSchema = z7.object({
1582
+ rows: z7.array(setupRowSchema)
1583
+ });
1584
+ var stateRequestSchema = z7.object({
1585
+ entities: z7.array(z7.string().min(1)),
1586
+ runId: z7.string().min(1),
1587
+ singletons: z7.array(z7.string().min(1)).default([])
1588
+ });
1589
+ var stateResponseSchema = z7.object({
1590
+ entities: z7.record(z7.string().min(1), z7.array(rowSchema)),
1591
+ singletons: z7.record(z7.string().min(1), cellSchema).default({})
1592
+ });
1593
+ var teardownRequestSchema = z7.object({
1594
+ runId: z7.string().min(1)
1595
+ });
1596
+ var teardownResponseSchema = z7.object({
1597
+ ok: z7.literal(true)
1598
+ });
1599
+
1600
+ // src/client-engine.ts
1601
+ var seedSchema = z8.record(z8.string(), z8.union([z8.string(), z8.number(), z8.boolean()])).catch({});
1602
+ function createClientEngine(_ripplo, impls) {
1603
+ const singletons = new Map(
1604
+ Object.entries(impls.singletons).map(([name, impl]) => [
1605
+ name,
1606
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- per-name impls typed at the call site; erase the contravariant scalar seed to the loose dispatch shape
1607
+ impl
1608
+ ])
1609
+ );
1610
+ const entities = new Map(Object.entries(impls.entities));
1611
+ const seed = seedSchema.parse(Reflect.get(globalThis, CLIENT_SEED_KEY));
1612
+ Object.entries(seed).forEach(([name, value2]) => singletons.get(name)?.seed(value2));
1241
1613
  return {
1242
- runId,
1243
- fixed: (value) => brandTestValue(value),
1244
- setCookie: (name, value, options) => {
1245
- const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
1246
- cookies.push({ name, options: resolvedOptions, value });
1247
- },
1248
- uniqueEmail: () => brandTestValue(`${TEST_ID_PREFIX}${nextSuffix()}@test.ripplo.ai`),
1249
- uniqueId: (prefix) => brandTestValue(`${TEST_ID_PREFIX}${prefix}-${nextSuffix()}`)
1614
+ readEntity: (name) => entities.get(name)?.read() ?? [],
1615
+ readSingleton: (name) => singletons.get(name)?.read() ?? null
1250
1616
  };
1251
1617
  }
1252
- function deriveDefaultDomain(baseUrl) {
1253
- if (baseUrl == null) {
1254
- return void 0;
1618
+ function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
1619
+ if (!enabled2) {
1620
+ return;
1255
1621
  }
1256
- try {
1257
- return new URL(baseUrl).hostname;
1258
- } catch {
1259
- return void 0;
1622
+ Reflect.set(globalThis, CLIENT_MOUNT_KEY, createClientEngine(ripplo, impls));
1623
+ }
1624
+
1625
+ // src/build.ts
1626
+ function buildLockfile(input) {
1627
+ assertUniqueNames(input.entities);
1628
+ const lockfile = lockfileSchema.parse({
1629
+ entities: input.entities.map((handle) => handle.schema),
1630
+ singletons: input.singletons.map((handle) => handle.schema),
1631
+ tests: input.tests.map((ripploTest) => ripploTest.spec),
1632
+ valueSpaces: dedupeByName([
1633
+ ...input.entities.flatMap((handle) => handle.valueSpaces),
1634
+ ...input.singletons.flatMap((handle) => handle.valueSpaces)
1635
+ ])
1636
+ });
1637
+ assertNoContradictions(lockfile);
1638
+ return lockfile;
1639
+ }
1640
+ function assertUniqueNames(entities) {
1641
+ const names = entities.map((handle) => handle.schema.name);
1642
+ const duplicate = names.find((name, index) => names.indexOf(name) !== index);
1643
+ if (duplicate != null) {
1644
+ throw new Error(`duplicate entity name "${duplicate}" \u2014 each entity name must be unique`);
1260
1645
  }
1261
1646
  }
1647
+ function dedupeByName(spaces) {
1648
+ const byName = new Map(spaces.map((space) => [space.name, space]));
1649
+ return [...byName.values()];
1650
+ }
1651
+ function assertNoContradictions(lockfile) {
1652
+ lockfile.tests.forEach((test2) => {
1653
+ assertNoContradiction(test2);
1654
+ assertNoDanglingRefs(test2);
1655
+ });
1656
+ }
1657
+ function assertNoContradiction(test2) {
1658
+ const setups = [...test2.world, ...test2.maybe];
1659
+ test2.absent.forEach((absence) => {
1660
+ const clash = setups.find(
1661
+ (setup) => setup.entity === absence.entity && whereMatches(absence.where, setup.set)
1662
+ );
1663
+ if (clash != null) {
1664
+ throw new Error(
1665
+ `test "${test2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
1666
+ );
1667
+ }
1668
+ });
1669
+ }
1670
+ function whereMatches(where, set) {
1671
+ return Object.entries(where).every(([field2, want]) => sameValue(want, set[field2]));
1672
+ }
1673
+ function sameValue(a, b) {
1674
+ return b !== void 0 && sameSetValue(a, b);
1675
+ }
1676
+ function assertNoDanglingRefs(test2) {
1677
+ const aliases = new Set([...test2.world, ...test2.maybe].map((setup) => setup.as));
1678
+ const paramKeys = new Set(Object.keys(test2.params));
1679
+ const fieldSets = [
1680
+ ...test2.world.map((setup) => setup.set),
1681
+ ...test2.maybe.map((setup) => setup.set),
1682
+ ...test2.absent.map((absence) => absence.where)
1683
+ ];
1684
+ fieldSets.forEach((set) => {
1685
+ assertSetRefs(test2.name, set, aliases, paramKeys);
1686
+ });
1687
+ }
1688
+ function assertSetRefs(testName, set, aliases, paramKeys) {
1689
+ Object.values(set).forEach((value2) => {
1690
+ if (!isRef(value2) || paramKeys.has(value2.ref)) {
1691
+ return;
1692
+ }
1693
+ const lastDot = value2.ref.lastIndexOf(".");
1694
+ if (lastDot === -1) {
1695
+ return;
1696
+ }
1697
+ const aliasPath = value2.ref.slice(0, lastDot);
1698
+ if (!aliases.has(aliasPath)) {
1699
+ throw new Error(
1700
+ `test "${testName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
1701
+ );
1702
+ }
1703
+ });
1704
+ }
1705
+ function isRef(value2) {
1706
+ return value2 != null && typeof value2 === "object" && "ref" in value2;
1707
+ }
1708
+
1709
+ // src/ripplo.ts
1710
+ function createRipplo(input) {
1711
+ return {
1712
+ entities: input.entities,
1713
+ lockfile: buildLockfile({
1714
+ entities: input.entities,
1715
+ singletons: input.singletons,
1716
+ tests: input.tests
1717
+ }),
1718
+ singletons: input.singletons,
1719
+ tests: input.tests
1720
+ };
1721
+ }
1262
1722
  export {
1263
- DEFAULT_IGNORE_PATHS,
1264
- DEFAULT_WATCH_PATHS,
1265
- TEST_ID_PREFIX,
1266
- compile,
1723
+ CLIENT_MOUNT_KEY,
1724
+ CLIENT_SEED_KEY,
1725
+ alert,
1726
+ alertdialog,
1727
+ and,
1728
+ arbitrary,
1729
+ banner,
1730
+ button,
1731
+ cell,
1732
+ changed,
1733
+ check,
1734
+ checkbox,
1735
+ clear,
1736
+ click,
1737
+ columnheader,
1738
+ combobox,
1739
+ complementary,
1740
+ contentinfo,
1741
+ count,
1742
+ createClientEngine,
1267
1743
  createEngine,
1268
1744
  createRipplo,
1269
- lint,
1270
- notImplemented,
1271
- observer,
1272
- precondition,
1273
- readAdapterWebhookSecret,
1274
- serializeCookie,
1745
+ dblclick,
1746
+ dialog,
1747
+ disabled,
1748
+ enabled,
1749
+ entity,
1750
+ field,
1751
+ fill,
1752
+ focused,
1753
+ form,
1754
+ goto,
1755
+ grid,
1756
+ group,
1757
+ heading,
1758
+ hover,
1759
+ id,
1760
+ img,
1761
+ inside,
1762
+ key,
1763
+ link,
1764
+ list,
1765
+ listitem,
1766
+ main,
1767
+ menu,
1768
+ menuitem,
1769
+ mountClientEngine,
1770
+ navigation,
1771
+ not,
1772
+ option,
1773
+ press,
1774
+ progressbar,
1775
+ radio,
1776
+ radiogroup,
1777
+ region,
1778
+ role,
1779
+ row,
1780
+ searchbox,
1781
+ select,
1782
+ singleton,
1783
+ slider,
1784
+ spinbutton,
1785
+ status,
1786
+ switchControl,
1787
+ tab,
1788
+ table,
1789
+ tablist,
1790
+ tabpanel,
1275
1791
  test,
1276
- toBatchRunResults,
1277
- toTeardownResults,
1278
- verifyWebhookSignature
1792
+ testId,
1793
+ text,
1794
+ textbox,
1795
+ title,
1796
+ toolbar,
1797
+ treeitem,
1798
+ uncheck,
1799
+ upload,
1800
+ url,
1801
+ v,
1802
+ value,
1803
+ viewport,
1804
+ visible,
1805
+ when,
1806
+ within
1279
1807
  };