@ripplo/testing 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -108,7 +108,13 @@ function not(input) {
108
108
  if (isConditionInput(input)) {
109
109
  return condLeaf({ kind: "not", predicate: input.predicate });
110
110
  }
111
- return leaf({ kind: "not", predicate: toPredicate(input) });
111
+ const predicate = toPredicate(input);
112
+ if (predicate.kind === "when") {
113
+ throw new Error(
114
+ "when() cannot be wrapped in not() \u2014 express the negation in branch conditions"
115
+ );
116
+ }
117
+ return leaf({ kind: "not", predicate });
112
118
  }
113
119
  function and(...conditions) {
114
120
  return condLeaf({ kind: "and", predicates: conditions.map((c) => c.predicate) });
@@ -118,26 +124,52 @@ function count(entity2) {
118
124
  is: (n) => condLeaf({ entity: entity2.name, kind: "count", value: n })
119
125
  };
120
126
  }
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");
127
+ function branch(name) {
128
+ const close = (condition) => (consequence) => {
129
+ const resolved = toPredicate(consequence);
130
+ if (containsWhen(resolved)) {
131
+ throw new Error(
132
+ `branch "${name}" nests a when() inside its consequence \u2014 whens are one level deep; split the scenario into another workflow`
133
+ );
134
+ }
135
+ return { __branch: true, branch: { condition, consequence: resolved, name } };
136
+ };
137
+ return {
138
+ expect: close(void 0),
139
+ if: (condition) => ({
140
+ expect: close(condition.predicate)
141
+ })
142
+ };
143
+ }
144
+ function when(...branches) {
145
+ if (branches.length === 0) {
146
+ throw new Error("when() needs at least one branch(...)");
147
+ }
148
+ const rows = branches.map((input) => input.branch);
149
+ const fallbacks = rows.filter((row2) => row2.condition == null);
150
+ if (fallbacks.length > 1) {
151
+ throw new Error("when() allows one unconditional branch \u2014 give the others .if(...) conditions");
152
+ }
153
+ if (fallbacks.length === 1 && rows.at(-1)?.condition != null) {
154
+ throw new Error("when() requires the unconditional branch to come last");
136
155
  }
137
- return leaf(chain);
156
+ const dup = rows.find((row2, index) => rows.findIndex((r) => r.name === row2.name) !== index);
157
+ if (dup != null) {
158
+ throw new Error(`when() branch name "${dup.name}" is used twice \u2014 branch names must be unique`);
159
+ }
160
+ return leaf({ branches: rows, kind: "when" });
138
161
  }
139
- function isClause(clause) {
140
- return Array.isArray(clause);
162
+ function containsWhen(predicate) {
163
+ if (predicate.kind === "when") {
164
+ return true;
165
+ }
166
+ if (predicate.kind === "not") {
167
+ return containsWhen(predicate.predicate);
168
+ }
169
+ if (predicate.kind === "and") {
170
+ return predicate.predicates.some((p) => containsWhen(p));
171
+ }
172
+ return false;
141
173
  }
142
174
 
143
175
  // src/util.ts
@@ -676,14 +708,17 @@ function assertPredicateScoped(predicate, given) {
676
708
  if (predicate.kind !== "when") {
677
709
  return;
678
710
  }
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
- }
711
+ predicate.branches.forEach((row2) => {
712
+ const names = row2.condition == null ? [] : conditionSingletons(row2.condition);
713
+ names.forEach((name) => {
714
+ if (!given.has(name)) {
715
+ throw new Error(
716
+ `when() conditions on singleton "${name}", which is not in the workflow's given \u2014 add ${name}.of(...) to given`
717
+ );
718
+ }
719
+ });
720
+ assertPredicateScoped(row2.consequence, given);
685
721
  });
686
- assertPredicateScoped(predicate.consequence, given);
687
722
  }
688
723
  function conditionSingletons(predicate) {
689
724
  if (predicate.kind === "singleton") {
@@ -724,7 +759,7 @@ function assignParams(bindings) {
724
759
  return {
725
760
  counts: { ...acc.counts, [token.base]: ordinal + 1 },
726
761
  names: new Map([...acc.names, [b, name]]),
727
- params: { ...acc.params, [name]: { example: token.example, valueSpace: token.valueSpace } }
762
+ params: { ...acc.params, [name]: { valueSpace: token.valueSpace } }
728
763
  };
729
764
  },
730
765
  { counts: {}, names: /* @__PURE__ */ new Map(), params: {} }
@@ -802,11 +837,10 @@ function predicateBindings(predicate) {
802
837
  return predicateBindings(predicate.predicate);
803
838
  }
804
839
  case "when": {
805
- return [
806
- ...predicateBindings(predicate.condition),
807
- ...predicateBindings(predicate.consequence),
808
- ...predicate.otherwise == null ? [] : predicateBindings(predicate.otherwise)
809
- ];
840
+ return predicate.branches.flatMap((row2) => [
841
+ ...row2.condition == null ? [] : predicateBindings(row2.condition),
842
+ ...predicateBindings(row2.consequence)
843
+ ]);
810
844
  }
811
845
  case "and": {
812
846
  return predicate.predicates.flatMap((p) => predicateBindings(p));
@@ -958,9 +992,11 @@ function resolvePredicate(predicate, ctx) {
958
992
  case "when": {
959
993
  return {
960
994
  ...predicate,
961
- condition: resolvePredicate(predicate.condition, ctx),
962
- consequence: resolvePredicate(predicate.consequence, ctx),
963
- otherwise: predicate.otherwise == null ? void 0 : resolvePredicate(predicate.otherwise, ctx)
995
+ branches: predicate.branches.map((row2) => ({
996
+ condition: row2.condition == null ? void 0 : resolveCondition(row2.condition, ctx),
997
+ consequence: resolvePredicate(row2.consequence, ctx),
998
+ name: row2.name
999
+ }))
964
1000
  };
965
1001
  }
966
1002
  case "and": {
@@ -1004,9 +1040,34 @@ function whereMap(map, ctx) {
1004
1040
  function resolveWhere(value2, ctx) {
1005
1041
  return isWithin2(value2) ? { ...value2, selection: { ...value2.selection, where: whereMap(value2.selection.where, ctx) } } : resolveValue(value2, ctx);
1006
1042
  }
1043
+ function resolveCondition(condition, ctx) {
1044
+ switch (condition.kind) {
1045
+ case "singleton": {
1046
+ return {
1047
+ ...condition,
1048
+ assertion: {
1049
+ ...condition.assertion,
1050
+ value: resolveValue(condition.assertion.value, ctx)
1051
+ }
1052
+ };
1053
+ }
1054
+ case "count": {
1055
+ return condition;
1056
+ }
1057
+ case "not": {
1058
+ return { ...condition, predicate: resolveCondition(condition.predicate, ctx) };
1059
+ }
1060
+ case "and": {
1061
+ return {
1062
+ ...condition,
1063
+ predicates: condition.predicates.map((p) => resolveCondition(p, ctx))
1064
+ };
1065
+ }
1066
+ }
1067
+ }
1007
1068
 
1008
- // src/test.ts
1009
- function test(intent, fn) {
1069
+ // src/workflow.ts
1070
+ function workflow(intent, fn) {
1010
1071
  const sourcePath = captureSourcePath();
1011
1072
  if (fn == null) {
1012
1073
  return { spec: stubSpec(intent, sourcePath) };
@@ -1024,6 +1085,7 @@ function test(intent, fn) {
1024
1085
  sourcePath,
1025
1086
  steps: final.steps,
1026
1087
  stub: false,
1088
+ tests: [],
1027
1089
  world: final.world
1028
1090
  }
1029
1091
  };
@@ -1040,32 +1102,32 @@ function stubSpec(intent, sourcePath) {
1040
1102
  sourcePath,
1041
1103
  steps: [],
1042
1104
  stub: true,
1105
+ tests: [],
1043
1106
  world: []
1044
1107
  };
1045
1108
  }
1046
- var TESTS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]tests[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
1109
+ var WORKFLOWS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]workflows[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
1047
1110
  function captureSourcePath() {
1048
1111
  const stack = new Error("capture").stack;
1049
1112
  if (stack == null) {
1050
1113
  return void 0;
1051
1114
  }
1052
- const match = stack.split("\n").map((line) => TESTS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
1115
+ const match = stack.split("\n").map((line) => WORKFLOWS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
1053
1116
  const captured = match?.[1];
1054
1117
  return captured == null ? void 0 : captured.replaceAll("\\", "/");
1055
1118
  }
1056
1119
  function slugify(intent) {
1057
1120
  const slug = intent.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim().split(" ").join("-");
1058
1121
  if (slug.length === 0) {
1059
- throw new Error(`test intent "${intent}" slugifies to an empty string`);
1122
+ throw new Error(`workflow intent "${intent}" slugifies to an empty string`);
1060
1123
  }
1061
1124
  return slug;
1062
1125
  }
1063
1126
 
1064
1127
  // src/params.ts
1065
- function arbitrary(field2, example) {
1128
+ function arbitrary(field2) {
1066
1129
  const token = {
1067
1130
  base: `${field2.entity}_${field2.field}`,
1068
- example,
1069
1131
  valueSpace: field2.valueSpaceName
1070
1132
  };
1071
1133
  return paramBinding(token);
@@ -1357,6 +1419,32 @@ var withinSchema = z2.object({
1357
1419
  selection: selectionSchema
1358
1420
  });
1359
1421
  var wait = z2.union([budgetSchema, z2.undefined()]).optional().transform((value2) => value2);
1422
+ var singletonPredicateSchema = z2.object({
1423
+ assertion: singletonAssertionSchema,
1424
+ kind: z2.literal("singleton"),
1425
+ singleton: z2.string().min(1),
1426
+ wait
1427
+ });
1428
+ var countPredicateSchema = z2.object({
1429
+ entity: z2.string().min(1),
1430
+ kind: z2.literal("count"),
1431
+ value: z2.number().int().nonnegative()
1432
+ });
1433
+ var conditionSchema = z2.lazy(
1434
+ () => z2.discriminatedUnion("kind", [
1435
+ singletonPredicateSchema,
1436
+ countPredicateSchema,
1437
+ z2.object({ kind: z2.literal("not"), predicate: conditionSchema }),
1438
+ z2.object({ kind: z2.literal("and"), predicates: z2.array(conditionSchema) })
1439
+ ])
1440
+ );
1441
+ var whenBranchSchema = z2.lazy(
1442
+ () => z2.object({
1443
+ condition: z2.union([conditionSchema, z2.undefined()]).optional().transform((value2) => value2),
1444
+ consequence: predicateSchema,
1445
+ name: z2.string().min(1)
1446
+ })
1447
+ );
1360
1448
  var predicateSchema = z2.lazy(
1361
1449
  () => z2.discriminatedUnion("kind", [
1362
1450
  z2.object({ kind: z2.literal("visible"), locator: locatorSchema, wait }),
@@ -1365,12 +1453,7 @@ var predicateSchema = z2.lazy(
1365
1453
  z2.object({ kind: z2.literal("focused"), locator: locatorSchema, wait }),
1366
1454
  z2.object({ kind: z2.literal("value"), locator: locatorSchema, value: stringValueSchema, wait }),
1367
1455
  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
- }),
1456
+ singletonPredicateSchema,
1374
1457
  z2.object({
1375
1458
  kind: z2.literal("browser"),
1376
1459
  name: browserSingletonSchema,
@@ -1386,17 +1469,8 @@ var predicateSchema = z2.lazy(
1386
1469
  }),
1387
1470
  z2.object({ kind: z2.literal("not"), predicate: predicateSchema }),
1388
1471
  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
- })
1472
+ countPredicateSchema,
1473
+ z2.object({ branches: z2.array(whenBranchSchema), kind: z2.literal("when") })
1400
1474
  ])
1401
1475
  );
1402
1476
 
@@ -1485,7 +1559,6 @@ var stepSchema = z4.object({
1485
1559
  expect: z4.array(predicateSchema).default([])
1486
1560
  });
1487
1561
  var paramSchema = z4.object({
1488
- example: primitiveSchema.optional(),
1489
1562
  valueSpace: z4.string().min(1)
1490
1563
  });
1491
1564
  var setupSchema = z4.object({
@@ -1497,7 +1570,19 @@ var absenceSchema = z4.object({
1497
1570
  entity: z4.string().min(1),
1498
1571
  where: z4.record(z4.string().min(1), setValueSchema)
1499
1572
  });
1500
- var testSchema = z4.object({
1573
+ var resolvedTestSchema = z4.object({
1574
+ absent: z4.array(absenceSchema).default([]),
1575
+ exclusive: z4.array(z4.string().min(1)).default([]),
1576
+ intent: z4.string().min(1),
1577
+ name: z4.string().min(1),
1578
+ params: z4.record(z4.string().min(1), paramSchema),
1579
+ singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
1580
+ slug: z4.string().min(1),
1581
+ steps: z4.array(stepSchema).default([]),
1582
+ workflow: z4.string().min(1),
1583
+ world: z4.array(setupSchema).default([])
1584
+ });
1585
+ var workflowSchema = z4.object({
1501
1586
  absent: z4.array(absenceSchema).default([]),
1502
1587
  exclusive: z4.array(z4.string().min(1)).default([]),
1503
1588
  intent: z4.string().min(1),
@@ -1508,6 +1593,7 @@ var testSchema = z4.object({
1508
1593
  sourcePath: z4.string().min(1).optional(),
1509
1594
  steps: z4.array(stepSchema).default([]),
1510
1595
  stub: z4.boolean().default(false),
1596
+ tests: z4.array(resolvedTestSchema).default([]),
1511
1597
  world: z4.array(setupSchema).default([])
1512
1598
  });
1513
1599
  var fixtureEntrySchema = z4.object({
@@ -1518,8 +1604,8 @@ var lockfileSchema = z4.object({
1518
1604
  entities: z4.array(entitySchemaSchema),
1519
1605
  fixtures: z4.record(z4.string().min(1), fixtureEntrySchema).default({}),
1520
1606
  singletons: z4.array(singletonSchemaSchema).default([]),
1521
- tests: z4.array(testSchema),
1522
- valueSpaces: z4.array(valueSpaceSchema)
1607
+ valueSpaces: z4.array(valueSpaceSchema),
1608
+ workflows: z4.array(workflowSchema)
1523
1609
  });
1524
1610
  var lockfileCodec = defineCodec({ name: "ripplo-lockfile", schema: lockfileSchema });
1525
1611
 
@@ -1531,6 +1617,9 @@ var stepDescriptorSchema = z5.object({
1531
1617
  target: z5.string(),
1532
1618
  value: z5.string()
1533
1619
  });
1620
+ function slugify2(name) {
1621
+ return name.toLowerCase().replaceAll(/[^a-z0-9]/g, "-").split("-").filter((part) => part.length > 0).join("-");
1622
+ }
1534
1623
 
1535
1624
  // ../spec/src/session.ts
1536
1625
  import { z as z6 } from "zod";
@@ -1618,17 +1707,491 @@ function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
1618
1707
  Reflect.set(globalThis, CLIENT_MOUNT_KEY, createClientEngine(ripplo, impls));
1619
1708
  }
1620
1709
 
1710
+ // src/expand.ts
1711
+ var MAX_CANDIDATES = 4096;
1712
+ function singletonValuesOf(singletons) {
1713
+ return new Map(
1714
+ singletons.flatMap((singleton2) => {
1715
+ const space = singleton2.valueSpaces.find((s) => s.name === singleton2.schema.valueSpace);
1716
+ return space?.values == null ? [] : [[singleton2.schema.name, space.values]];
1717
+ })
1718
+ );
1719
+ }
1720
+ function expandWorkflow(workflow2, options) {
1721
+ if (workflow2.stub) {
1722
+ return { ...workflow2, tests: [] };
1723
+ }
1724
+ assertUniqueBranchNames(workflow2);
1725
+ const targets = collectTargets(workflow2.steps);
1726
+ const whenIds = new Map(targets.map((target, index) => [target.when, index]));
1727
+ const sims = proposeCandidates(workflow2, options).map((candidate) => simulate(workflow2, candidate)).filter((sim) => sim != null);
1728
+ if (targets.length === 0) {
1729
+ return { ...workflow2, tests: [requireMainTest(workflow2, sims)] };
1730
+ }
1731
+ const { tests } = targets.reduce(
1732
+ (acc, target) => coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }),
1733
+ { covered: /* @__PURE__ */ new Set(), tests: [] }
1734
+ );
1735
+ return { ...workflow2, tests };
1736
+ }
1737
+ function resolveValidTest(workflow2, sim, name) {
1738
+ const test = resolveTest(workflow2, sim, name);
1739
+ return danglingRefHeads(test).length === 0 ? test : null;
1740
+ }
1741
+ function danglingRefHeads(test) {
1742
+ const known = /* @__PURE__ */ new Set([
1743
+ ...test.world.map((setup) => setup.as),
1744
+ ...Object.keys(test.params),
1745
+ ...test.steps.flatMap((step) => createdAliasesIn(step))
1746
+ ]);
1747
+ return [...new Set(collectRefObjects(test).map((ref) => headOf(ref)))].filter(
1748
+ (head) => !known.has(head)
1749
+ );
1750
+ }
1751
+ function createdAliasesIn(step) {
1752
+ return step.expect.flatMap((predicate) => {
1753
+ if (predicate.kind !== "state" || predicate.assertion.kind === "deleted") {
1754
+ return [];
1755
+ }
1756
+ return [predicate.assertion.as];
1757
+ });
1758
+ }
1759
+ function coverTarget({ acc, sims, target, whenIds, workflow: workflow2 }) {
1760
+ const key2 = choiceKey(whenIds, target.when, target.index);
1761
+ if (acc.covered.has(key2)) {
1762
+ return { covered: acc.covered, tests: acc.tests };
1763
+ }
1764
+ const picked = sims.filter((s) => s.choices.get(target.when) === target.index).reduce((found, s) => {
1765
+ if (found != null) {
1766
+ return found;
1767
+ }
1768
+ const test = resolveValidTest(workflow2, s, target.name);
1769
+ return test == null ? null : { sim: s, test };
1770
+ }, null);
1771
+ if (picked == null) {
1772
+ throw new Error(
1773
+ `workflow "${workflow2.name}": branch "${target.name}" is unreachable \u2014 no combination of optional entities and singleton values reaches it`
1774
+ );
1775
+ }
1776
+ const reached = [...picked.sim.choices].map(([when2, index]) => choiceKey(whenIds, when2, index));
1777
+ return {
1778
+ covered: /* @__PURE__ */ new Set([...acc.covered, ...reached]),
1779
+ tests: [...acc.tests, picked.test]
1780
+ };
1781
+ }
1782
+ function choiceKey(whenIds, when2, index) {
1783
+ const id2 = whenIds.get(when2);
1784
+ if (id2 == null) {
1785
+ throw new Error("internal: when node missing from target index");
1786
+ }
1787
+ return `${String(id2)}:${String(index)}`;
1788
+ }
1789
+ function requireMainTest(workflow2, sims) {
1790
+ const test = sims.reduce(
1791
+ (found, sim) => found ?? resolveValidTest(workflow2, sim, "main"),
1792
+ null
1793
+ );
1794
+ if (test == null) {
1795
+ throw new Error(
1796
+ `workflow "${workflow2.name}": no valid seed found \u2014 steps may reference optional entities no candidate provides, or a condition mentions values the solver cannot pin`
1797
+ );
1798
+ }
1799
+ return test;
1800
+ }
1801
+ function assertUniqueBranchNames(workflow2) {
1802
+ const names = collectTargets(workflow2.steps).map((target) => target.name);
1803
+ const dup = names.find((name, index) => names.indexOf(name) !== index);
1804
+ if (dup != null) {
1805
+ throw new Error(
1806
+ `workflow "${workflow2.name}": branch name "${dup}" is used twice \u2014 branch names must be unique within a workflow`
1807
+ );
1808
+ }
1809
+ }
1810
+ function collectTargets(steps) {
1811
+ return steps.flatMap((step) => step.expect.flatMap((predicate) => targetsIn(predicate)));
1812
+ }
1813
+ function targetsIn(predicate) {
1814
+ if (predicate.kind === "not") {
1815
+ return targetsIn(predicate.predicate);
1816
+ }
1817
+ if (predicate.kind === "and") {
1818
+ return predicate.predicates.flatMap((p) => targetsIn(p));
1819
+ }
1820
+ if (predicate.kind !== "when") {
1821
+ return [];
1822
+ }
1823
+ return predicate.branches.flatMap((row2, index) => [
1824
+ { index, name: row2.name, when: predicate },
1825
+ ...targetsIn(row2.consequence)
1826
+ ]);
1827
+ }
1828
+ function proposeCandidates(workflow2, options) {
1829
+ const subsets = maybeSubsets(workflow2.maybe);
1830
+ const pinSets = pinAssignments(workflow2, options);
1831
+ if (subsets.length * pinSets.length > MAX_CANDIDATES) {
1832
+ throw new Error(
1833
+ `workflow "${workflow2.name}": too many optional entities and singleton values to solve \u2014 split the workflow or convert maybe(...) entities to of(...)`
1834
+ );
1835
+ }
1836
+ return subsets.flatMap((maybes) => pinSets.map((pins) => ({ maybes, pins })));
1837
+ }
1838
+ function maybeSubsets(maybe) {
1839
+ const masks = Array.from({ length: 1 << maybe.length }, (_, mask) => mask);
1840
+ return masks.map((mask) => ({ mask, size: maybe.filter((_, i) => (mask & 1 << i) !== 0).length })).toSorted((a, b) => a.size === b.size ? a.mask - b.mask : a.size - b.size).map(({ mask }) => maybe.filter((_, i) => (mask & 1 << i) !== 0));
1841
+ }
1842
+ function pinAssignments(workflow2, options) {
1843
+ const domains = pinDomains(workflow2, options);
1844
+ return Object.entries(domains).reduce(
1845
+ (acc, [param, domain]) => acc.flatMap((pins) => domain.map((value2) => ({ ...pins, [param]: value2 }))),
1846
+ [{}]
1847
+ );
1848
+ }
1849
+ function pinDomains(workflow2, options) {
1850
+ const literals = conditionSingletonLiterals(workflow2.steps);
1851
+ return Object.entries(workflow2.singletons).reduce((acc, [name, value2]) => {
1852
+ const param = paramRefOf(value2);
1853
+ const compared = literals.get(name);
1854
+ if (param == null || compared == null) {
1855
+ return acc;
1856
+ }
1857
+ const enumValues = options.singletonValues.get(name);
1858
+ return { ...acc, [param]: withComplements(compared, enumValues) };
1859
+ }, {});
1860
+ }
1861
+ function conditionSingletonLiterals(steps) {
1862
+ const pairs = steps.flatMap(
1863
+ (step) => step.expect.flatMap((predicate) => conditionLiteralsIn(predicate))
1864
+ );
1865
+ return pairs.reduce(
1866
+ (acc, [name, literal]) => new Map([...acc, [name, [.../* @__PURE__ */ new Set([...acc.get(name) ?? [], literal])]]]),
1867
+ /* @__PURE__ */ new Map()
1868
+ );
1869
+ }
1870
+ function conditionLiteralsIn(predicate) {
1871
+ if (predicate.kind === "not") {
1872
+ return conditionLiteralsIn(predicate.predicate);
1873
+ }
1874
+ if (predicate.kind === "and") {
1875
+ return predicate.predicates.flatMap((p) => conditionLiteralsIn(p));
1876
+ }
1877
+ if (predicate.kind !== "when") {
1878
+ return [];
1879
+ }
1880
+ return predicate.branches.flatMap((row2) => [
1881
+ ...row2.condition == null ? [] : singletonLiteralsInCondition(row2.condition),
1882
+ ...conditionLiteralsIn(row2.consequence)
1883
+ ]);
1884
+ }
1885
+ function singletonLiteralsInCondition(condition) {
1886
+ switch (condition.kind) {
1887
+ case "singleton": {
1888
+ const value2 = condition.assertion.value;
1889
+ return value2 == null || typeof value2 !== "object" ? [[condition.singleton, value2]] : [];
1890
+ }
1891
+ case "count": {
1892
+ return [];
1893
+ }
1894
+ case "not": {
1895
+ return singletonLiteralsInCondition(condition.predicate);
1896
+ }
1897
+ case "and": {
1898
+ return condition.predicates.flatMap((p) => singletonLiteralsInCondition(p));
1899
+ }
1900
+ }
1901
+ }
1902
+ function paramRefOf(value2) {
1903
+ if (value2 == null || typeof value2 !== "object" || !("ref" in value2)) {
1904
+ return void 0;
1905
+ }
1906
+ return value2.ref.includes(".") ? void 0 : value2.ref;
1907
+ }
1908
+ function withComplements(values, enumValues) {
1909
+ if (enumValues != null) {
1910
+ return [.../* @__PURE__ */ new Set([...values, ...enumValues])];
1911
+ }
1912
+ const complements = values.flatMap((value2) => {
1913
+ if (typeof value2 === "boolean") {
1914
+ return [!value2];
1915
+ }
1916
+ if (typeof value2 === "number") {
1917
+ return [value2 + 1];
1918
+ }
1919
+ if (typeof value2 === "string") {
1920
+ return [syntheticDistinct(value2, values)];
1921
+ }
1922
+ return [];
1923
+ });
1924
+ return [.../* @__PURE__ */ new Set([...values, ...complements])];
1925
+ }
1926
+ function syntheticDistinct(seed, taken) {
1927
+ const candidate = `${seed}-alt`;
1928
+ return taken.includes(candidate) ? syntheticDistinct(candidate, taken) : candidate;
1929
+ }
1930
+ function simulate(workflow2, candidate) {
1931
+ const initial = {
1932
+ choices: /* @__PURE__ */ new Map(),
1933
+ state: {
1934
+ rows: [...workflow2.world, ...candidate.maybes],
1935
+ singles: seedSingles(workflow2.singletons, candidate.pins)
1936
+ }
1937
+ };
1938
+ const folded = workflow2.steps.reduce(
1939
+ (acc, step) => acc == null ? null : stepSim(acc, step),
1940
+ initial
1941
+ );
1942
+ return folded == null ? null : { candidate, choices: folded.choices };
1943
+ }
1944
+ function seedSingles(singletons, pins) {
1945
+ return Object.fromEntries(
1946
+ Object.entries(singletons).map(([name, value2]) => {
1947
+ const param = paramRefOf(value2);
1948
+ return [name, param != null && param in pins ? pins[param] ?? null : value2];
1949
+ })
1950
+ );
1951
+ }
1952
+ function stepSim(acc, step) {
1953
+ const rows = step.expect.filter((p) => p.kind === "state").reduce((current, effect) => applyEffect(current, effect), acc.state.rows);
1954
+ const preWhens = { rows, singles: acc.state.singles };
1955
+ const whens = step.expect.filter((p) => p.kind === "when");
1956
+ const resolved = whens.reduce((current, when2) => current == null ? null : foldWhen(current, when2, preWhens), {
1957
+ choices: acc.choices,
1958
+ sets: {}
1959
+ });
1960
+ if (resolved == null) {
1961
+ return null;
1962
+ }
1963
+ const immediate = singletonSets(step.expect);
1964
+ return {
1965
+ choices: resolved.choices,
1966
+ state: { rows, singles: { ...preWhens.singles, ...immediate, ...resolved.sets } }
1967
+ };
1968
+ }
1969
+ function foldWhen(acc, when2, state) {
1970
+ const picked = pickBranch(when2, state);
1971
+ if (picked === "unknown") {
1972
+ return null;
1973
+ }
1974
+ if (picked == null) {
1975
+ return acc;
1976
+ }
1977
+ const row2 = when2.branches[picked];
1978
+ if (row2 == null) {
1979
+ return acc;
1980
+ }
1981
+ const choices = new Map([...acc.choices, [when2, picked]]);
1982
+ const consequence = row2.consequence;
1983
+ if (consequence.kind === "when") {
1984
+ return foldWhen({ choices, sets: acc.sets }, consequence, state);
1985
+ }
1986
+ return { choices, sets: { ...acc.sets, ...singletonSets([consequence]) } };
1987
+ }
1988
+ function pickBranch(when2, state) {
1989
+ return when2.branches.reduce((picked, row2, index) => {
1990
+ if (picked !== void 0) {
1991
+ return picked;
1992
+ }
1993
+ if (row2.condition == null) {
1994
+ return index;
1995
+ }
1996
+ const holds = evalCondition(row2.condition, state);
1997
+ if (holds === "unknown") {
1998
+ return "unknown";
1999
+ }
2000
+ return holds ? index : void 0;
2001
+ }, void 0);
2002
+ }
2003
+ function evalCondition(condition, state) {
2004
+ switch (condition.kind) {
2005
+ case "count": {
2006
+ return state.rows.filter((row2) => row2.entity === condition.entity).length === condition.value;
2007
+ }
2008
+ case "singleton": {
2009
+ return compareValues(state.singles[condition.singleton], condition.assertion.value);
2010
+ }
2011
+ case "not": {
2012
+ return negate(evalCondition(condition.predicate, state));
2013
+ }
2014
+ case "and": {
2015
+ return conjoin(condition.predicates.map((p) => evalCondition(p, state)));
2016
+ }
2017
+ }
2018
+ }
2019
+ function compareValues(seeded, wanted) {
2020
+ if (seeded === void 0) {
2021
+ return "unknown";
2022
+ }
2023
+ const seededRef = isRefValue(seeded);
2024
+ const wantedRef = isRefValue(wanted);
2025
+ if (seededRef != null && wantedRef != null) {
2026
+ return seededRef === wantedRef ? true : "unknown";
2027
+ }
2028
+ if (seededRef != null || wantedRef != null || isTemplateValue(seeded) || isTemplateValue(wanted)) {
2029
+ return "unknown";
2030
+ }
2031
+ return sameSetValue(seeded, wanted);
2032
+ }
2033
+ function isRefValue(value2) {
2034
+ return value2 != null && typeof value2 === "object" && "ref" in value2 ? value2.ref : void 0;
2035
+ }
2036
+ function isTemplateValue(value2) {
2037
+ return value2 != null && typeof value2 === "object" && "template" in value2;
2038
+ }
2039
+ function negate(value2) {
2040
+ return value2 === "unknown" ? "unknown" : !value2;
2041
+ }
2042
+ function conjoin(values) {
2043
+ if (values.includes(false)) {
2044
+ return false;
2045
+ }
2046
+ return values.every((v2) => v2 === true) ? true : "unknown";
2047
+ }
2048
+ function singletonSets(predicates) {
2049
+ return Object.fromEntries(
2050
+ predicates.flatMap((predicate) => {
2051
+ if (predicate.kind === "singleton") {
2052
+ return [[predicate.singleton, predicate.assertion.value]];
2053
+ }
2054
+ return [];
2055
+ })
2056
+ );
2057
+ }
2058
+ function applyEffect(rows, effect) {
2059
+ if (effect.assertion.kind === "created") {
2060
+ return [
2061
+ ...rows,
2062
+ { as: effect.assertion.as, entity: effect.entity, set: effect.assertion.props }
2063
+ ];
2064
+ }
2065
+ if (effect.assertion.kind === "deleted") {
2066
+ return rows.filter((row2) => row2.entity !== effect.entity || !matchesKey(row2, effect.key, rows));
2067
+ }
2068
+ return rows;
2069
+ }
2070
+ function matchesKey(row2, key2, rows) {
2071
+ return Object.entries(key2).every(([field2, want]) => matchesField({ field: field2, row: row2, rows, want }));
2072
+ }
2073
+ function matchesField({ field: field2, row: row2, rows, want }) {
2074
+ if (isWithinValue(want)) {
2075
+ const candidates = rows.filter(
2076
+ (candidate) => candidate.entity === want.selection.entity && matchesKey(candidate, want.selection.where, rows)
2077
+ ).map((candidate) => fieldValue(candidate, want.field));
2078
+ const own = fieldValue(row2, field2);
2079
+ return candidates.some((candidate) => valuesEqual(own, candidate));
2080
+ }
2081
+ return valuesEqual(fieldValue(row2, field2), want);
2082
+ }
2083
+ function isWithinValue(value2) {
2084
+ return value2 != null && typeof value2 === "object" && "kind" in value2;
2085
+ }
2086
+ function fieldValue(row2, field2) {
2087
+ return row2.set[field2] ?? { ref: `${row2.as}.${field2}` };
2088
+ }
2089
+ function valuesEqual(a, b) {
2090
+ const aRef = isRefValue(a);
2091
+ const bRef = isRefValue(b);
2092
+ if (aRef != null || bRef != null) {
2093
+ return aRef === bRef;
2094
+ }
2095
+ if (isTemplateValue(a) || isTemplateValue(b)) {
2096
+ return false;
2097
+ }
2098
+ return sameSetValue(a, b);
2099
+ }
2100
+ function resolveTest(workflow2, sim, name) {
2101
+ const steps = workflow2.steps.map((step) => ({
2102
+ action: step.action,
2103
+ expect: step.expect.flatMap((predicate) => resolvePredicate2(predicate, sim.choices))
2104
+ }));
2105
+ const world = [...workflow2.world, ...sim.candidate.maybes];
2106
+ const singletons = seedSingles(workflow2.singletons, sim.candidate.pins);
2107
+ const used = usedRefHeads({ singletons, steps, workflow: workflow2, world });
2108
+ return {
2109
+ absent: workflow2.absent,
2110
+ exclusive: workflow2.exclusive,
2111
+ intent: workflow2.intent,
2112
+ name,
2113
+ params: Object.fromEntries(
2114
+ Object.entries(workflow2.params).filter(([param]) => used.has(param))
2115
+ ),
2116
+ singletons,
2117
+ slug: slugify2(name),
2118
+ steps,
2119
+ workflow: workflow2.name,
2120
+ world
2121
+ };
2122
+ }
2123
+ function resolvePredicate2(predicate, choices) {
2124
+ if (predicate.kind === "not") {
2125
+ const inner = resolvePredicate2(predicate.predicate, choices);
2126
+ return inner.map((p) => ({ kind: "not", predicate: p }));
2127
+ }
2128
+ if (predicate.kind === "and") {
2129
+ return [
2130
+ {
2131
+ kind: "and",
2132
+ predicates: predicate.predicates.flatMap((p) => resolvePredicate2(p, choices))
2133
+ }
2134
+ ];
2135
+ }
2136
+ if (predicate.kind !== "when") {
2137
+ return [predicate];
2138
+ }
2139
+ const picked = choices.get(predicate);
2140
+ const row2 = picked == null ? void 0 : predicate.branches[picked];
2141
+ return row2 == null ? [] : resolvePredicate2(row2.consequence, choices);
2142
+ }
2143
+ function usedRefHeads({ singletons, steps, workflow: workflow2, world }) {
2144
+ const values = [
2145
+ ...world.flatMap((setup) => Object.values(setup.set)),
2146
+ ...workflow2.absent.flatMap((absence) => Object.values(absence.where)),
2147
+ ...Object.values(singletons),
2148
+ ...steps.flatMap((step) => stepRefValues(step))
2149
+ ];
2150
+ return new Set(values.flatMap((value2) => refStrings(value2)).map((ref) => headOf(ref)));
2151
+ }
2152
+ function stepRefValues(step) {
2153
+ return collectRefObjects(step).map((ref) => ({ ref }));
2154
+ }
2155
+ function collectRefObjects(node) {
2156
+ if (Array.isArray(node)) {
2157
+ return node.flatMap((item) => collectRefObjects(item));
2158
+ }
2159
+ if (node == null || typeof node !== "object") {
2160
+ return [];
2161
+ }
2162
+ if ("ref" in node && typeof node.ref === "string" && Object.keys(node).length === 1) {
2163
+ return [node.ref];
2164
+ }
2165
+ return Object.values(node).flatMap((value2) => collectRefObjects(value2));
2166
+ }
2167
+ function refStrings(value2) {
2168
+ const ref = isRefValue(value2);
2169
+ if (ref != null) {
2170
+ return [ref];
2171
+ }
2172
+ if (value2 != null && typeof value2 === "object" && "template" in value2) {
2173
+ return value2.template.flatMap((segment) => typeof segment === "string" ? [] : [segment.ref]);
2174
+ }
2175
+ return [];
2176
+ }
2177
+ function headOf(ref) {
2178
+ const dot = ref.indexOf(".");
2179
+ return dot === -1 ? ref : ref.slice(0, dot);
2180
+ }
2181
+
1621
2182
  // src/build.ts
1622
2183
  function buildLockfile(input) {
1623
2184
  assertUniqueNames(input.entities);
1624
2185
  const lockfile = lockfileSchema.parse({
1625
2186
  entities: input.entities.map((handle) => handle.schema),
1626
2187
  singletons: input.singletons.map((handle) => handle.schema),
1627
- tests: input.tests.map((ripploTest) => ripploTest.spec),
1628
2188
  valueSpaces: dedupeByName([
1629
2189
  ...input.entities.flatMap((handle) => handle.valueSpaces),
1630
2190
  ...input.singletons.flatMap((handle) => handle.valueSpaces)
1631
- ])
2191
+ ]),
2192
+ workflows: input.workflows.map(
2193
+ (ripploWorkflow) => expandWorkflow(ripploWorkflow.spec, { singletonValues: singletonValuesOf(input.singletons) })
2194
+ )
1632
2195
  });
1633
2196
  assertNoContradictions(lockfile);
1634
2197
  return lockfile;
@@ -1645,20 +2208,20 @@ function dedupeByName(spaces) {
1645
2208
  return [...byName.values()];
1646
2209
  }
1647
2210
  function assertNoContradictions(lockfile) {
1648
- lockfile.tests.forEach((test2) => {
1649
- assertNoContradiction(test2);
1650
- assertNoDanglingRefs(test2);
2211
+ lockfile.workflows.forEach((workflow2) => {
2212
+ assertNoContradiction(workflow2);
2213
+ assertNoDanglingRefs(workflow2);
1651
2214
  });
1652
2215
  }
1653
- function assertNoContradiction(test2) {
1654
- const setups = [...test2.world, ...test2.maybe];
1655
- test2.absent.forEach((absence) => {
2216
+ function assertNoContradiction(workflow2) {
2217
+ const setups = [...workflow2.world, ...workflow2.maybe];
2218
+ workflow2.absent.forEach((absence) => {
1656
2219
  const clash = setups.find(
1657
2220
  (setup) => setup.entity === absence.entity && whereMatches(absence.where, setup.set)
1658
2221
  );
1659
2222
  if (clash != null) {
1660
2223
  throw new Error(
1661
- `test "${test2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
2224
+ `test "${workflow2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
1662
2225
  );
1663
2226
  }
1664
2227
  });
@@ -1669,19 +2232,19 @@ function whereMatches(where, set) {
1669
2232
  function sameValue(a, b) {
1670
2233
  return b !== void 0 && sameSetValue(a, b);
1671
2234
  }
1672
- function assertNoDanglingRefs(test2) {
1673
- const aliases = new Set([...test2.world, ...test2.maybe].map((setup) => setup.as));
1674
- const paramKeys = new Set(Object.keys(test2.params));
2235
+ function assertNoDanglingRefs(workflow2) {
2236
+ const aliases = new Set([...workflow2.world, ...workflow2.maybe].map((setup) => setup.as));
2237
+ const paramKeys = new Set(Object.keys(workflow2.params));
1675
2238
  const fieldSets = [
1676
- ...test2.world.map((setup) => setup.set),
1677
- ...test2.maybe.map((setup) => setup.set),
1678
- ...test2.absent.map((absence) => absence.where)
2239
+ ...workflow2.world.map((setup) => setup.set),
2240
+ ...workflow2.maybe.map((setup) => setup.set),
2241
+ ...workflow2.absent.map((absence) => absence.where)
1679
2242
  ];
1680
2243
  fieldSets.forEach((set) => {
1681
- assertSetRefs(test2.name, set, aliases, paramKeys);
2244
+ assertSetRefs(workflow2.name, set, aliases, paramKeys);
1682
2245
  });
1683
2246
  }
1684
- function assertSetRefs(testName, set, aliases, paramKeys) {
2247
+ function assertSetRefs(workflowName, set, aliases, paramKeys) {
1685
2248
  Object.values(set).forEach((value2) => {
1686
2249
  if (!isRef(value2) || paramKeys.has(value2.ref)) {
1687
2250
  return;
@@ -1693,7 +2256,7 @@ function assertSetRefs(testName, set, aliases, paramKeys) {
1693
2256
  const aliasPath = value2.ref.slice(0, lastDot);
1694
2257
  if (!aliases.has(aliasPath)) {
1695
2258
  throw new Error(
1696
- `test "${testName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
2259
+ `test "${workflowName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
1697
2260
  );
1698
2261
  }
1699
2262
  });
@@ -1709,10 +2272,10 @@ function createRipplo(input) {
1709
2272
  lockfile: buildLockfile({
1710
2273
  entities: input.entities,
1711
2274
  singletons: input.singletons,
1712
- tests: input.tests
2275
+ workflows: input.workflows
1713
2276
  }),
1714
2277
  singletons: input.singletons,
1715
- tests: input.tests
2278
+ workflows: input.workflows
1716
2279
  };
1717
2280
  }
1718
2281
  export {
@@ -1723,6 +2286,7 @@ export {
1723
2286
  and,
1724
2287
  arbitrary,
1725
2288
  banner,
2289
+ branch,
1726
2290
  button,
1727
2291
  cell,
1728
2292
  changed,
@@ -1784,7 +2348,6 @@ export {
1784
2348
  table,
1785
2349
  tablist,
1786
2350
  tabpanel,
1787
- test,
1788
2351
  testId,
1789
2352
  text,
1790
2353
  textbox,
@@ -1799,5 +2362,6 @@ export {
1799
2362
  viewport,
1800
2363
  visible,
1801
2364
  when,
1802
- within
2365
+ within,
2366
+ workflow
1803
2367
  };