@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.
- package/DSL.md +357 -0
- package/LICENSE.md +1 -0
- package/README.md +47 -273
- package/dist/engine-BfvzXgLg.d.ts +1091 -0
- package/dist/express.d.ts +7 -9
- package/dist/express.js +422 -48
- package/dist/index.d.ts +134 -59
- package/dist/index.js +1654 -1126
- package/package.json +31 -113
- package/dist/actions.d.ts +0 -260
- package/dist/actions.js +0 -177
- package/dist/assert.d.ts +0 -188
- package/dist/assert.js +0 -111
- package/dist/builder-SsgqYqSC.d.ts +0 -156
- package/dist/chunk-2YLI7VD4.js +0 -65
- package/dist/chunk-4MGIQFAJ.js +0 -16
- package/dist/chunk-DCJBLS2U.js +0 -26
- package/dist/chunk-MGATMMCZ.js +0 -16
- package/dist/chunk-XO36IU66.js +0 -88
- package/dist/chunk-YFOTJIVF.js +0 -134
- package/dist/chunk-YQAEOH5W.js +0 -111
- package/dist/compiler.d.ts +0 -32
- package/dist/compiler.js +0 -8
- package/dist/control.d.ts +0 -45
- package/dist/control.js +0 -17
- package/dist/elysia.d.ts +0 -78
- package/dist/elysia.js +0 -114
- package/dist/engine-BOqzK_go.d.ts +0 -61
- package/dist/fastify.d.ts +0 -14
- package/dist/fastify.js +0 -79
- package/dist/hono.d.ts +0 -19
- package/dist/hono.js +0 -89
- package/dist/koa.d.ts +0 -14
- package/dist/koa.js +0 -135
- package/dist/locators.d.ts +0 -40
- package/dist/locators.js +0 -11
- package/dist/lockfile.d.ts +0 -722
- package/dist/lockfile.js +0 -707
- package/dist/nestjs.d.ts +0 -17
- package/dist/nestjs.js +0 -139
- package/dist/nextjs.d.ts +0 -14
- package/dist/nextjs.js +0 -137
- package/dist/step-De52hTLd.d.ts +0 -19
- package/dist/types-BzZrl65Z.d.ts +0 -115
package/dist/index.js
CHANGED
|
@@ -1,1279 +1,1807 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
])
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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/
|
|
72
|
-
function
|
|
73
|
-
return
|
|
54
|
+
// src/predicates.ts
|
|
55
|
+
function isCapturePredicate(input) {
|
|
56
|
+
return "ref" in input;
|
|
74
57
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
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
|
|
79
|
-
return {
|
|
64
|
+
function leaf(predicate) {
|
|
65
|
+
return { predicate, wait: (budget) => leaf(withWait(predicate, budget)) };
|
|
80
66
|
}
|
|
81
|
-
function
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
function condLeaf(predicate) {
|
|
68
|
+
return {
|
|
69
|
+
__condition: true,
|
|
70
|
+
predicate,
|
|
71
|
+
wait: (budget) => condLeaf(withWaitCondition(predicate, budget))
|
|
72
|
+
};
|
|
85
73
|
}
|
|
86
|
-
function
|
|
87
|
-
return
|
|
88
|
-
input: () => buildReady(state)
|
|
89
|
-
});
|
|
74
|
+
function isConditionInput(input) {
|
|
75
|
+
return "__condition" in input;
|
|
90
76
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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/
|
|
100
|
-
function
|
|
101
|
-
return
|
|
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
|
|
104
|
-
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
145
|
-
const name = readPreconditionName(p);
|
|
256
|
+
function key(options) {
|
|
146
257
|
return {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
158
|
-
const name = readObserverName(o);
|
|
270
|
+
function within(selection, sourceField) {
|
|
159
271
|
return {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
201
|
-
|
|
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
|
|
204
|
-
return Object.
|
|
356
|
+
function matchPropsOf(props) {
|
|
357
|
+
return Object.fromEntries(
|
|
358
|
+
Object.entries(props).filter((entry) => !isChanged(entry[1]))
|
|
359
|
+
);
|
|
205
360
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
376
|
+
ref,
|
|
377
|
+
wait: (budget) => build(budget)
|
|
222
378
|
});
|
|
223
|
-
return
|
|
379
|
+
return build(void 0);
|
|
224
380
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
248
|
-
return
|
|
406
|
+
function inside(scope, target) {
|
|
407
|
+
return { by: "inside", scope, target };
|
|
249
408
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
name
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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/
|
|
380
|
-
function
|
|
381
|
-
|
|
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
|
|
420
|
-
|
|
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
|
|
438
|
-
|
|
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
|
|
452
|
-
|
|
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
|
|
463
|
-
|
|
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
|
|
481
|
-
|
|
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
|
|
501
|
-
|
|
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
|
|
514
|
-
|
|
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
|
|
526
|
-
|
|
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
|
|
538
|
-
return
|
|
551
|
+
function upload(locator, files) {
|
|
552
|
+
return stepBuilder({ files: [...files], kind: "upload", locator }, [], []);
|
|
539
553
|
}
|
|
540
|
-
function
|
|
541
|
-
return
|
|
554
|
+
function press(pressKey, locator) {
|
|
555
|
+
return stepBuilder({ key: pressKey, kind: "press", locator }, [], []);
|
|
542
556
|
}
|
|
543
|
-
function
|
|
544
|
-
return
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
562
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
574
|
-
|
|
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
|
|
592
|
-
if (
|
|
593
|
-
return
|
|
633
|
+
function valueBindings(value2) {
|
|
634
|
+
if (isBinding(value2)) {
|
|
635
|
+
return [value2];
|
|
594
636
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return `role:${loc.role}:${loc.name ?? ""}`;
|
|
637
|
+
if (isTemplate(value2)) {
|
|
638
|
+
return value2.template.flatMap((segment) => isBinding(segment) ? [segment] : []);
|
|
598
639
|
}
|
|
599
|
-
return
|
|
640
|
+
return [];
|
|
600
641
|
}
|
|
601
|
-
function
|
|
602
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return fromLabel;
|
|
648
|
+
function emitReady(done, remaining) {
|
|
649
|
+
if (remaining.length === 0) {
|
|
650
|
+
return [...done];
|
|
693
651
|
}
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
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
|
|
699
|
-
|
|
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
|
-
|
|
703
|
-
if (outcomeTokens.size === 0) {
|
|
676
|
+
if (predicate.kind !== "when") {
|
|
704
677
|
return;
|
|
705
678
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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 (
|
|
727
|
-
return
|
|
761
|
+
if (action.kind === "upload") {
|
|
762
|
+
return locatorBindings(action.locator);
|
|
728
763
|
}
|
|
729
|
-
|
|
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
|
|
734
|
-
if (
|
|
735
|
-
return;
|
|
766
|
+
function locatorBindings(locator) {
|
|
767
|
+
if (locator.by === "inside") {
|
|
768
|
+
return [...locatorBindings(locator.scope), ...locatorBindings(locator.target)];
|
|
736
769
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
742
|
-
|
|
783
|
+
case "value":
|
|
784
|
+
case "text": {
|
|
785
|
+
return [...locatorBindings(predicate.locator), ...valueBindings(predicate.value)];
|
|
743
786
|
}
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
753
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
return
|
|
774
|
-
});
|
|
775
|
-
if (anyReferencesVariable) {
|
|
776
|
-
return;
|
|
801
|
+
case "not": {
|
|
802
|
+
return predicateBindings(predicate.predicate);
|
|
777
803
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
791
|
-
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
rule: "no-literal-template-strings",
|
|
828
|
-
step: node.label ?? node.id
|
|
829
|
-
});
|
|
830
|
-
});
|
|
814
|
+
case "count": {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
}
|
|
831
818
|
}
|
|
832
|
-
|
|
833
|
-
|
|
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
|
|
878
|
-
return
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
|
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
|
|
896
|
-
const name = readPreconditionName(handle);
|
|
856
|
+
function resolveTemplate(template, ctx) {
|
|
897
857
|
return {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
909
|
-
|
|
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
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
|
961
|
-
const name = readObserverName(handle);
|
|
962
|
-
const typedImpl = impl;
|
|
873
|
+
function resolveStep(step, ctx) {
|
|
963
874
|
return {
|
|
964
|
-
|
|
965
|
-
|
|
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
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
|
983
|
-
|
|
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
|
-
|
|
986
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1043
|
-
return
|
|
939
|
+
case "singleton": {
|
|
940
|
+
return {
|
|
941
|
+
...predicate,
|
|
942
|
+
assertion: { ...predicate.assertion, value: resolveValue(predicate.assertion.value, ctx) }
|
|
943
|
+
};
|
|
1044
944
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
return `Unknown precondition: "${name}"`;
|
|
945
|
+
case "browser": {
|
|
946
|
+
return { ...predicate, value: resolveString(predicate.value, ctx) };
|
|
1048
947
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
948
|
+
case "state": {
|
|
949
|
+
return {
|
|
950
|
+
...predicate,
|
|
951
|
+
assertion: resolveAssertion(predicate.assertion, ctx),
|
|
952
|
+
key: whereMap(predicate.key, ctx)
|
|
953
|
+
};
|
|
1055
954
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
968
|
+
...predicate,
|
|
969
|
+
predicates: predicate.predicates.map((p) => resolvePredicate(p, ctx))
|
|
1120
970
|
};
|
|
1121
971
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
|
1131
|
-
if (
|
|
1132
|
-
return
|
|
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
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
|
1031
|
+
function stubSpec(intent, sourcePath) {
|
|
1145
1032
|
return {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
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
|
|
1253
|
-
if (
|
|
1254
|
-
return
|
|
1618
|
+
function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
|
|
1619
|
+
if (!enabled2) {
|
|
1620
|
+
return;
|
|
1255
1621
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
};
|