@ripplo/testing 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DSL.md +355 -0
- package/LICENSE.md +1 -0
- package/README.md +47 -273
- package/dist/engine-BT7hUouB.d.ts +1095 -0
- package/dist/express.d.ts +7 -9
- package/dist/express.js +422 -48
- package/dist/index.d.ts +122 -59
- package/dist/index.js +1630 -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,1783 @@
|
|
|
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 button = named("button");
|
|
416
|
+
var cell = named("cell");
|
|
417
|
+
var checkbox = named("checkbox");
|
|
418
|
+
var combobox = named("combobox");
|
|
419
|
+
var dialog = named("dialog");
|
|
420
|
+
var heading = named("heading");
|
|
421
|
+
var img = named("img");
|
|
422
|
+
var link = named("link");
|
|
423
|
+
var listitem = named("listitem");
|
|
424
|
+
var menuitem = named("menuitem");
|
|
425
|
+
var option = named("option");
|
|
426
|
+
var radio = named("radio");
|
|
427
|
+
var row = named("row");
|
|
428
|
+
var searchbox = named("searchbox");
|
|
429
|
+
var tab = named("tab");
|
|
430
|
+
var textbox = named("textbox");
|
|
431
|
+
var alert = container("alert");
|
|
432
|
+
var banner = container("banner");
|
|
433
|
+
var complementary = container("complementary");
|
|
434
|
+
var contentinfo = container("contentinfo");
|
|
435
|
+
var form = container("form");
|
|
436
|
+
var list = container("list");
|
|
437
|
+
var main = container("main");
|
|
438
|
+
var menu = container("menu");
|
|
439
|
+
var navigation = container("navigation");
|
|
440
|
+
var region = container("region");
|
|
441
|
+
var status = container("status");
|
|
442
|
+
var table = container("table");
|
|
443
|
+
var tablist = container("tablist");
|
|
444
|
+
function named(roleName) {
|
|
445
|
+
return (name, ...bindings) => role(roleName, name, ...bindings);
|
|
446
|
+
}
|
|
447
|
+
function container(roleName) {
|
|
448
|
+
return (name, ...bindings) => role(roleName, name, ...bindings);
|
|
449
|
+
}
|
|
450
|
+
function nameValue(name, bindings) {
|
|
451
|
+
if (typeof name === "string") {
|
|
452
|
+
return name;
|
|
453
|
+
}
|
|
454
|
+
if (isBinding(name)) {
|
|
455
|
+
return name;
|
|
456
|
+
}
|
|
457
|
+
return stringValueFromTemplate(name, bindings);
|
|
282
458
|
}
|
|
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
|
-
|
|
459
|
+
|
|
460
|
+
// src/singleton.ts
|
|
461
|
+
function singleton(name, config) {
|
|
462
|
+
const valueSpaceName = `singleton.${name}`;
|
|
463
|
+
const { constraints, generator, primitive } = config.value;
|
|
464
|
+
const schema = {
|
|
465
|
+
consistency: config.consistency ?? "strict",
|
|
466
|
+
default: config.default,
|
|
467
|
+
description: config.description,
|
|
468
|
+
name,
|
|
469
|
+
source: config.source,
|
|
470
|
+
type: primitive,
|
|
471
|
+
valueSpace: valueSpaceName
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
is: isPredicate,
|
|
475
|
+
name,
|
|
476
|
+
schema,
|
|
477
|
+
source: config.source,
|
|
478
|
+
value: { __t: void 0, entity: name, field: "value", primitive, valueSpaceName },
|
|
479
|
+
valueSpaces: [{ constraints, generator, name: valueSpaceName, type: primitive }],
|
|
480
|
+
of: (value2) => ({
|
|
481
|
+
__entity: { kind: "singletonState", singleton: name, value: toSetValue(value2) },
|
|
482
|
+
is: isPredicate
|
|
483
|
+
})
|
|
484
|
+
};
|
|
485
|
+
function isPredicate(value2) {
|
|
486
|
+
return condLeaf({
|
|
487
|
+
assertion: { kind: "is", value: value2 },
|
|
488
|
+
kind: "singleton",
|
|
489
|
+
singleton: name,
|
|
490
|
+
wait: void 0
|
|
491
|
+
});
|
|
492
|
+
}
|
|
307
493
|
}
|
|
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,
|
|
494
|
+
|
|
495
|
+
// src/builtins.ts
|
|
496
|
+
function browserSingleton(name) {
|
|
497
|
+
return {
|
|
498
|
+
name,
|
|
499
|
+
is: (strings, ...values) => leaf({
|
|
500
|
+
kind: "browser",
|
|
325
501
|
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
|
-
})
|
|
502
|
+
value: stringValueFromTemplate(strings, values),
|
|
503
|
+
wait: void 0
|
|
375
504
|
})
|
|
376
|
-
}
|
|
505
|
+
};
|
|
377
506
|
}
|
|
507
|
+
var url = browserSingleton("url");
|
|
508
|
+
var title = browserSingleton("title");
|
|
509
|
+
var viewport = browserSingleton("viewport");
|
|
378
510
|
|
|
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
|
-
});
|
|
511
|
+
// src/actions.ts
|
|
512
|
+
function goto(strings, ...values) {
|
|
513
|
+
return stepBuilder({ kind: "goto", url: stringValueFromTemplate(strings, values) }, [], []);
|
|
418
514
|
}
|
|
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
|
-
});
|
|
515
|
+
function click(locator) {
|
|
516
|
+
return stepBuilder({ kind: "click", locator }, [], []);
|
|
436
517
|
}
|
|
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
|
-
}
|
|
518
|
+
function dblclick(locator) {
|
|
519
|
+
return stepBuilder({ kind: "dblclick", locator }, [], []);
|
|
450
520
|
}
|
|
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
|
-
});
|
|
521
|
+
function fill(locator, binding) {
|
|
522
|
+
return stepBuilder({ kind: "fill", locator, value: binding }, [], []);
|
|
461
523
|
}
|
|
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
|
-
});
|
|
524
|
+
function clear(locator) {
|
|
525
|
+
return stepBuilder({ kind: "clear", locator }, [], []);
|
|
479
526
|
}
|
|
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
|
-
});
|
|
527
|
+
function select(locator, binding) {
|
|
528
|
+
return stepBuilder({ kind: "select", locator, value: binding }, [], []);
|
|
499
529
|
}
|
|
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
|
-
}
|
|
530
|
+
function check(locator) {
|
|
531
|
+
return stepBuilder({ kind: "check", locator }, [], []);
|
|
512
532
|
}
|
|
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
|
-
}
|
|
533
|
+
function uncheck(locator) {
|
|
534
|
+
return stepBuilder({ kind: "uncheck", locator }, [], []);
|
|
524
535
|
}
|
|
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;
|
|
536
|
+
function hover(locator) {
|
|
537
|
+
return stepBuilder({ kind: "hover", locator }, [], []);
|
|
536
538
|
}
|
|
537
|
-
function
|
|
538
|
-
return
|
|
539
|
+
function upload(locator, files) {
|
|
540
|
+
return stepBuilder({ files: [...files], kind: "upload", locator }, [], []);
|
|
539
541
|
}
|
|
540
|
-
function
|
|
541
|
-
return
|
|
542
|
+
function press(pressKey, locator) {
|
|
543
|
+
return stepBuilder({ key: pressKey, kind: "press", locator }, [], []);
|
|
542
544
|
}
|
|
543
|
-
function
|
|
544
|
-
return
|
|
545
|
+
function stepBuilder(action, expected, captures) {
|
|
546
|
+
return {
|
|
547
|
+
captures,
|
|
548
|
+
step: { action, expect: [...expected] },
|
|
549
|
+
expect: (...predicates) => stepBuilder(
|
|
550
|
+
action,
|
|
551
|
+
[...expected, ...predicates.map((p) => toPredicate(p))],
|
|
552
|
+
[...captures, ...predicates.flatMap((p) => isCapturePredicate(p) ? [captureOf(p)] : [])]
|
|
553
|
+
)
|
|
554
|
+
};
|
|
545
555
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
"
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
556
|
+
|
|
557
|
+
// src/finalize.ts
|
|
558
|
+
function finalize(body) {
|
|
559
|
+
const descriptors = [...new Set(body.given.map((item) => item.__entity))];
|
|
560
|
+
const absences = descriptors.filter((d) => d.kind === "none");
|
|
561
|
+
const singletonStates = descriptors.filter(
|
|
562
|
+
(d) => d.kind === "singletonState"
|
|
563
|
+
);
|
|
564
|
+
const entities = topoSort(
|
|
565
|
+
descriptors.filter(
|
|
566
|
+
(d) => d.kind === "of" || d.kind === "only" || d.kind === "maybe"
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
assertNoMaybeRefs({ absences, entities, singletonStates });
|
|
570
|
+
const captures = body.steps.flatMap((builder) => builder.captures);
|
|
571
|
+
assertScopedConditions(body.steps, new Set(singletonStates.map((s) => s.singleton)));
|
|
572
|
+
const aliases = assignAliases([...entities, ...captures.map((c) => c.descriptor)]);
|
|
573
|
+
const { names, params } = assignParams(allBindings(body, entities, absences, singletonStates));
|
|
574
|
+
const ctx = {
|
|
575
|
+
aliases,
|
|
576
|
+
captures: new Map(
|
|
577
|
+
captures.map((c) => [c.assertion, c.descriptor])
|
|
578
|
+
),
|
|
579
|
+
params: names
|
|
580
|
+
};
|
|
581
|
+
return {
|
|
582
|
+
absent: absences.map((a) => ({ entity: a.entity, where: setMap(a.where, ctx) })),
|
|
583
|
+
exclusive: [...new Set(entities.filter((d) => d.kind === "only").map((d) => d.entity))],
|
|
584
|
+
maybe: setupsOf(entities, "maybe", ctx),
|
|
585
|
+
params,
|
|
586
|
+
singletons: resolveSingletons(singletonStates, ctx),
|
|
587
|
+
steps: body.steps.map((builder) => resolveStep(builder.step, ctx)),
|
|
588
|
+
world: entities.filter((d) => d.kind === "of" || d.kind === "only").map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }))
|
|
589
|
+
};
|
|
560
590
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
591
|
+
function resolveSingletons(states, ctx) {
|
|
592
|
+
return states.reduce((acc, state) => {
|
|
593
|
+
const value2 = resolveValue(state.value, ctx);
|
|
594
|
+
const existing = acc[state.singleton];
|
|
595
|
+
if (existing !== void 0 && !sameSetValue(existing, value2)) {
|
|
596
|
+
throw new Error(
|
|
597
|
+
`singleton "${state.singleton}" is given two conflicting values \u2014 a test may set each singleton to one value`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return { ...acc, [state.singleton]: value2 };
|
|
601
|
+
}, {});
|
|
602
|
+
}
|
|
603
|
+
function assertNoMaybeRefs({ absences, entities, singletonStates }) {
|
|
604
|
+
const offenders = [
|
|
605
|
+
...entities.filter((d) => d.kind === "of" || d.kind === "only").flatMap((d) => maybeBindings(Object.values(d.props), `${d.kind}(${d.entity})`)),
|
|
606
|
+
...absences.flatMap((a) => maybeBindings(Object.values(a.where), `none(${a.entity})`)),
|
|
607
|
+
...singletonStates.flatMap((s) => maybeBindings([s.value], `singleton "${s.singleton}"`))
|
|
608
|
+
];
|
|
609
|
+
const first = offenders[0];
|
|
610
|
+
if (first == null || first.binding.__bind.kind !== "field") {
|
|
563
611
|
return;
|
|
564
612
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
step: void 0
|
|
570
|
-
});
|
|
571
|
-
}
|
|
613
|
+
const { descriptor, field: field2 } = first.binding.__bind;
|
|
614
|
+
throw new Error(
|
|
615
|
+
`${first.site} references ${descriptor.entity}.${field2} from a maybe(${descriptor.entity}) \u2014 maybe entities are not materialized at setup; reference them only in steps`
|
|
616
|
+
);
|
|
572
617
|
}
|
|
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
|
-
}
|
|
618
|
+
function maybeBindings(values, site) {
|
|
619
|
+
return values.flatMap((value2) => valueBindings(value2)).filter((b) => b.__bind.kind === "field" && b.__bind.descriptor.kind === "maybe").map((binding) => ({ binding, site }));
|
|
590
620
|
}
|
|
591
|
-
function
|
|
592
|
-
if (
|
|
593
|
-
return
|
|
621
|
+
function valueBindings(value2) {
|
|
622
|
+
if (isBinding(value2)) {
|
|
623
|
+
return [value2];
|
|
594
624
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
return `role:${loc.role}:${loc.name ?? ""}`;
|
|
625
|
+
if (isTemplate(value2)) {
|
|
626
|
+
return value2.template.flatMap((segment) => isBinding(segment) ? [segment] : []);
|
|
598
627
|
}
|
|
599
|
-
return
|
|
628
|
+
return [];
|
|
600
629
|
}
|
|
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
|
-
});
|
|
630
|
+
function isTemplate(value2) {
|
|
631
|
+
return typeof value2 === "object" && value2 !== null && "template" in value2;
|
|
629
632
|
}
|
|
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));
|
|
633
|
+
function topoSort(entities) {
|
|
634
|
+
return emitReady([], entities);
|
|
688
635
|
}
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return fromLabel;
|
|
636
|
+
function emitReady(done, remaining) {
|
|
637
|
+
if (remaining.length === 0) {
|
|
638
|
+
return [...done];
|
|
693
639
|
}
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
640
|
+
const ready = remaining.filter(
|
|
641
|
+
(d) => depsOf(d, remaining).every((dep) => done.includes(dep) || dep === d)
|
|
642
|
+
);
|
|
643
|
+
if (ready.length === 0) {
|
|
644
|
+
throw new Error("cyclic dependency between given entities");
|
|
645
|
+
}
|
|
646
|
+
return emitReady(
|
|
647
|
+
[...done, ...ready],
|
|
648
|
+
remaining.filter((d) => !ready.includes(d))
|
|
649
|
+
);
|
|
697
650
|
}
|
|
698
|
-
function
|
|
699
|
-
|
|
651
|
+
function depsOf(descriptor, among) {
|
|
652
|
+
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));
|
|
653
|
+
}
|
|
654
|
+
function assertScopedConditions(steps, givenSingletons) {
|
|
655
|
+
steps.flatMap((builder) => builder.step.expect).forEach((predicate) => {
|
|
656
|
+
assertPredicateScoped(predicate, givenSingletons);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function assertPredicateScoped(predicate, given) {
|
|
660
|
+
if (predicate.kind === "not") {
|
|
661
|
+
assertPredicateScoped(predicate.predicate, given);
|
|
700
662
|
return;
|
|
701
663
|
}
|
|
702
|
-
|
|
703
|
-
if (outcomeTokens.size === 0) {
|
|
664
|
+
if (predicate.kind !== "when") {
|
|
704
665
|
return;
|
|
705
666
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
667
|
+
conditionSingletons(predicate.condition).forEach((name) => {
|
|
668
|
+
if (!given.has(name)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`when() conditions on singleton "${name}", which is not in the test's given \u2014 add ${name}.of(...) to given`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
assertPredicateScoped(predicate.consequence, given);
|
|
675
|
+
}
|
|
676
|
+
function conditionSingletons(predicate) {
|
|
677
|
+
if (predicate.kind === "singleton") {
|
|
678
|
+
return [predicate.singleton];
|
|
709
679
|
}
|
|
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
|
-
});
|
|
680
|
+
if (predicate.kind === "not") {
|
|
681
|
+
return conditionSingletons(predicate.predicate);
|
|
682
|
+
}
|
|
683
|
+
if (predicate.kind === "and") {
|
|
684
|
+
return predicate.predicates.flatMap((p) => conditionSingletons(p));
|
|
719
685
|
}
|
|
686
|
+
return [];
|
|
687
|
+
}
|
|
688
|
+
function assignAliases(entities) {
|
|
689
|
+
return entities.reduce(
|
|
690
|
+
(acc, d) => {
|
|
691
|
+
const ordinal = acc.counts[d.entity] ?? 0;
|
|
692
|
+
return {
|
|
693
|
+
aliases: new Map([...acc.aliases, [d, `${d.entity}_${String(ordinal)}`]]),
|
|
694
|
+
counts: { ...acc.counts, [d.entity]: ordinal + 1 }
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
{ aliases: /* @__PURE__ */ new Map(), counts: {} }
|
|
698
|
+
).aliases;
|
|
699
|
+
}
|
|
700
|
+
function assignParams(bindings) {
|
|
701
|
+
const unique = [...new Set(bindings.filter((b) => b.__bind.kind === "param"))];
|
|
702
|
+
const assigned = unique.reduce(
|
|
703
|
+
(acc, b) => {
|
|
704
|
+
const token = tokenOf(b);
|
|
705
|
+
const ordinal = acc.counts[token.base] ?? 0;
|
|
706
|
+
const name = ordinal === 0 ? token.base : `${token.base}_${String(ordinal - 1)}`;
|
|
707
|
+
if (name in acc.params) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`param name "${name}" collides with an existing param \u2014 rename the field so deduplicated param names stay unique`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
counts: { ...acc.counts, [token.base]: ordinal + 1 },
|
|
714
|
+
names: new Map([...acc.names, [b, name]]),
|
|
715
|
+
params: { ...acc.params, [name]: { example: token.example, valueSpace: token.valueSpace } }
|
|
716
|
+
};
|
|
717
|
+
},
|
|
718
|
+
{ counts: {}, names: /* @__PURE__ */ new Map(), params: {} }
|
|
719
|
+
);
|
|
720
|
+
return { names: assigned.names, params: assigned.params };
|
|
720
721
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
722
|
+
function tokenOf(binding) {
|
|
723
|
+
if (binding.__bind.kind !== "param") {
|
|
724
|
+
throw new Error("internal: expected a param binding");
|
|
725
|
+
}
|
|
726
|
+
return binding.__bind.token;
|
|
727
|
+
}
|
|
728
|
+
function allBindings(body, entities, absences, singletonStates) {
|
|
729
|
+
return [
|
|
730
|
+
...entities.flatMap((d) => Object.values(d.props).flatMap((v2) => valueBindings(v2))),
|
|
731
|
+
...absences.flatMap((a) => Object.values(a.where).flatMap((v2) => valueBindings(v2))),
|
|
732
|
+
...singletonStates.flatMap((s) => valueBindings(s.value)),
|
|
733
|
+
...body.steps.flatMap((builder) => stepBindings(builder.step))
|
|
734
|
+
];
|
|
735
|
+
}
|
|
736
|
+
function stepBindings(step) {
|
|
737
|
+
return [...actionBindings(step.action), ...step.expect.flatMap((p) => predicateBindings(p))];
|
|
738
|
+
}
|
|
739
|
+
function actionBindings(action) {
|
|
740
|
+
if (action.kind === "goto") {
|
|
741
|
+
return valueBindings(action.url);
|
|
742
|
+
}
|
|
743
|
+
if (action.kind === "fill" || action.kind === "select") {
|
|
744
|
+
return [...locatorBindings(action.locator), ...valueBindings(action.value)];
|
|
745
|
+
}
|
|
746
|
+
if (action.kind === "press") {
|
|
747
|
+
return action.locator == null ? [] : locatorBindings(action.locator);
|
|
725
748
|
}
|
|
726
|
-
if (
|
|
727
|
-
return
|
|
749
|
+
if (action.kind === "upload") {
|
|
750
|
+
return locatorBindings(action.locator);
|
|
728
751
|
}
|
|
729
|
-
|
|
730
|
-
const name = loc.by === "role" ? loc.name ?? "" : loc.value;
|
|
731
|
-
return BACKEND_MUTATION_KEYWORDS.test(name);
|
|
752
|
+
return locatorBindings(action.locator);
|
|
732
753
|
}
|
|
733
|
-
function
|
|
734
|
-
if (
|
|
735
|
-
return;
|
|
754
|
+
function locatorBindings(locator) {
|
|
755
|
+
if (locator.by === "inside") {
|
|
756
|
+
return [...locatorBindings(locator.scope), ...locatorBindings(locator.target)];
|
|
736
757
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
758
|
+
if (locator.by === "role") {
|
|
759
|
+
return locator.name == null ? [] : valueBindings(locator.name);
|
|
760
|
+
}
|
|
761
|
+
return valueBindings(locator.value);
|
|
762
|
+
}
|
|
763
|
+
function predicateBindings(predicate) {
|
|
764
|
+
switch (predicate.kind) {
|
|
765
|
+
case "visible":
|
|
766
|
+
case "disabled":
|
|
767
|
+
case "enabled":
|
|
768
|
+
case "focused": {
|
|
769
|
+
return locatorBindings(predicate.locator);
|
|
740
770
|
}
|
|
741
|
-
|
|
742
|
-
|
|
771
|
+
case "value":
|
|
772
|
+
case "text": {
|
|
773
|
+
return [...locatorBindings(predicate.locator), ...valueBindings(predicate.value)];
|
|
743
774
|
}
|
|
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;
|
|
775
|
+
case "singleton": {
|
|
776
|
+
return valueBindings(predicate.assertion.value);
|
|
751
777
|
}
|
|
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;
|
|
778
|
+
case "browser": {
|
|
779
|
+
return valueBindings(predicate.value);
|
|
767
780
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
781
|
+
case "state": {
|
|
782
|
+
return [
|
|
783
|
+
...predicate.assertion.kind === "deleted" ? [] : Object.values(predicate.assertion.props).flatMap(
|
|
784
|
+
(v2) => isChanged(v2) ? [] : valueBindings(v2)
|
|
785
|
+
),
|
|
786
|
+
...Object.values(predicate.key).flatMap((v2) => whereBindings(v2))
|
|
787
|
+
];
|
|
771
788
|
}
|
|
772
|
-
|
|
773
|
-
return
|
|
774
|
-
});
|
|
775
|
-
if (anyReferencesVariable) {
|
|
776
|
-
return;
|
|
789
|
+
case "not": {
|
|
790
|
+
return predicateBindings(predicate.predicate);
|
|
777
791
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
function uploadFixtureName(nodes, _test, report) {
|
|
786
|
-
nodes.forEach((node) => {
|
|
787
|
-
if (node.type !== "upload") {
|
|
788
|
-
return;
|
|
792
|
+
case "when": {
|
|
793
|
+
return [
|
|
794
|
+
...predicateBindings(predicate.condition),
|
|
795
|
+
...predicateBindings(predicate.consequence),
|
|
796
|
+
...predicate.otherwise == null ? [] : predicateBindings(predicate.otherwise)
|
|
797
|
+
];
|
|
789
798
|
}
|
|
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;
|
|
799
|
+
case "and": {
|
|
800
|
+
return predicate.predicates.flatMap((p) => predicateBindings(p));
|
|
822
801
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
rule: "no-literal-template-strings",
|
|
828
|
-
step: node.label ?? node.id
|
|
829
|
-
});
|
|
830
|
-
});
|
|
802
|
+
case "count": {
|
|
803
|
+
return [];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
831
806
|
}
|
|
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
|
-
};
|
|
807
|
+
function whereBindings(value2) {
|
|
808
|
+
return isWithin2(value2) ? Object.values(value2.selection.where).flatMap((v2) => whereBindings(v2)) : valueBindings(value2);
|
|
876
809
|
}
|
|
877
|
-
function
|
|
878
|
-
return
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
});
|
|
810
|
+
function isWithin2(value2) {
|
|
811
|
+
return typeof value2 === "object" && value2 !== null && "kind" in value2;
|
|
812
|
+
}
|
|
813
|
+
function setMap(map, ctx) {
|
|
814
|
+
return Object.fromEntries(
|
|
815
|
+
Object.entries(map).map(([key2, value2]) => [key2, resolveValue(value2, ctx)])
|
|
816
|
+
);
|
|
885
817
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
818
|
+
function resolveValue(value2, ctx) {
|
|
819
|
+
if (isBinding(value2)) {
|
|
820
|
+
return resolveBinding(value2, ctx);
|
|
821
|
+
}
|
|
822
|
+
if (isTemplate(value2)) {
|
|
823
|
+
return resolveTemplate(value2, ctx);
|
|
824
|
+
}
|
|
825
|
+
return value2;
|
|
826
|
+
}
|
|
827
|
+
function resolveBinding(binding, ctx) {
|
|
828
|
+
const bind = binding.__bind;
|
|
829
|
+
if (bind.kind === "param") {
|
|
830
|
+
const name = ctx.params.get(binding);
|
|
831
|
+
if (name == null) {
|
|
832
|
+
throw new Error("internal: param binding was not collected");
|
|
891
833
|
}
|
|
892
|
-
return
|
|
893
|
-
}
|
|
834
|
+
return { ref: name };
|
|
835
|
+
}
|
|
836
|
+
const alias = ctx.aliases.get(bind.descriptor);
|
|
837
|
+
if (alias == null) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`references a "${bind.descriptor.entity}" entity that is not included in the test's given`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
return { ref: `${alias}.${bind.field}` };
|
|
894
843
|
}
|
|
895
|
-
function
|
|
896
|
-
const name = readPreconditionName(handle);
|
|
844
|
+
function resolveTemplate(template, ctx) {
|
|
897
845
|
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}`))
|
|
846
|
+
template: template.template.map(
|
|
847
|
+
(segment) => isBinding(segment) ? resolveBinding(segment, ctx) : segment
|
|
848
|
+
)
|
|
906
849
|
};
|
|
907
850
|
}
|
|
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
|
-
};
|
|
851
|
+
function setupsOf(entities, kind, ctx) {
|
|
852
|
+
return entities.filter((d) => d.kind === kind).map((d) => ({ as: aliasFor(ctx, d), entity: d.entity, set: setMap(d.props, ctx) }));
|
|
949
853
|
}
|
|
950
|
-
function
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
name,
|
|
957
|
-
run: () => Promise.resolve(createFailOutcome(`observer "${name}" is ${sentinel.reason}`))
|
|
958
|
-
};
|
|
854
|
+
function aliasFor(ctx, descriptor) {
|
|
855
|
+
const alias = ctx.aliases.get(descriptor);
|
|
856
|
+
if (alias == null) {
|
|
857
|
+
throw new Error(`internal: no alias for ${descriptor.entity}`);
|
|
858
|
+
}
|
|
859
|
+
return alias;
|
|
959
860
|
}
|
|
960
|
-
function
|
|
961
|
-
const name = readObserverName(handle);
|
|
962
|
-
const typedImpl = impl;
|
|
861
|
+
function resolveStep(step, ctx) {
|
|
963
862
|
return {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
implemented: true,
|
|
967
|
-
name,
|
|
968
|
-
run: async (ctx, params) => {
|
|
969
|
-
return typedImpl(ctx, params);
|
|
970
|
-
}
|
|
863
|
+
action: resolveAction(step.action, ctx),
|
|
864
|
+
expect: step.expect.map((predicate) => resolvePredicate(predicate, ctx))
|
|
971
865
|
};
|
|
972
866
|
}
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
867
|
+
function resolveAction(action, ctx) {
|
|
868
|
+
if (action.kind === "goto") {
|
|
869
|
+
return { ...action, url: resolveString(action.url, ctx) };
|
|
870
|
+
}
|
|
871
|
+
if (action.kind === "fill" || action.kind === "select") {
|
|
872
|
+
return {
|
|
873
|
+
...action,
|
|
874
|
+
locator: resolveLocator(action.locator, ctx),
|
|
875
|
+
value: resolveValue(action.value, ctx)
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
if (action.kind === "press") {
|
|
879
|
+
return {
|
|
880
|
+
...action,
|
|
881
|
+
locator: action.locator == null ? void 0 : resolveLocator(action.locator, ctx)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (action.kind === "upload") {
|
|
885
|
+
return { ...action, locator: resolveLocator(action.locator, ctx) };
|
|
886
|
+
}
|
|
887
|
+
return { ...action, locator: resolveLocator(action.locator, ctx) };
|
|
981
888
|
}
|
|
982
|
-
function
|
|
983
|
-
|
|
889
|
+
function resolveString(value2, ctx) {
|
|
890
|
+
if (isBinding(value2)) {
|
|
891
|
+
return resolveBinding(value2, ctx);
|
|
892
|
+
}
|
|
893
|
+
if (isTemplate(value2)) {
|
|
894
|
+
return resolveTemplate(value2, ctx);
|
|
895
|
+
}
|
|
896
|
+
return value2;
|
|
984
897
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
items,
|
|
988
|
-
options
|
|
989
|
-
}) {
|
|
990
|
-
const defaultDomain = deriveDefaultDomain(options?.appUrl);
|
|
991
|
-
const initial = items.map((item) => {
|
|
992
|
-
const cookiesRef = [];
|
|
898
|
+
function resolveLocator(locator, ctx) {
|
|
899
|
+
if (locator.by === "inside") {
|
|
993
900
|
return {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
data: {},
|
|
998
|
-
executed: [],
|
|
999
|
-
failure: void 0,
|
|
1000
|
-
names: item.names,
|
|
1001
|
-
runId: item.runId
|
|
901
|
+
by: "inside",
|
|
902
|
+
scope: resolveLocator(locator.scope, ctx),
|
|
903
|
+
target: resolveLocator(locator.target, ctx)
|
|
1002
904
|
};
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
905
|
+
}
|
|
906
|
+
if (locator.by === "role") {
|
|
907
|
+
return locator.name == null ? locator : { ...locator, name: resolveString(locator.name, ctx) };
|
|
908
|
+
}
|
|
909
|
+
return { ...locator, value: resolveString(locator.value, ctx) };
|
|
910
|
+
}
|
|
911
|
+
function resolvePredicate(predicate, ctx) {
|
|
912
|
+
switch (predicate.kind) {
|
|
913
|
+
case "visible":
|
|
914
|
+
case "disabled":
|
|
915
|
+
case "enabled":
|
|
916
|
+
case "focused": {
|
|
917
|
+
return { ...predicate, locator: resolveLocator(predicate.locator, ctx) };
|
|
1016
918
|
}
|
|
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;
|
|
919
|
+
case "value":
|
|
920
|
+
case "text": {
|
|
921
|
+
return {
|
|
922
|
+
...predicate,
|
|
923
|
+
locator: resolveLocator(predicate.locator, ctx),
|
|
924
|
+
value: resolveString(predicate.value, ctx)
|
|
925
|
+
};
|
|
1041
926
|
}
|
|
1042
|
-
|
|
1043
|
-
return
|
|
927
|
+
case "singleton": {
|
|
928
|
+
return {
|
|
929
|
+
...predicate,
|
|
930
|
+
assertion: { ...predicate.assertion, value: resolveValue(predicate.assertion.value, ctx) }
|
|
931
|
+
};
|
|
1044
932
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
return `Unknown precondition: "${name}"`;
|
|
933
|
+
case "browser": {
|
|
934
|
+
return { ...predicate, value: resolveString(predicate.value, ctx) };
|
|
1048
935
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
936
|
+
case "state": {
|
|
937
|
+
return {
|
|
938
|
+
...predicate,
|
|
939
|
+
assertion: resolveAssertion(predicate.assertion, ctx),
|
|
940
|
+
key: whereMap(predicate.key, ctx)
|
|
941
|
+
};
|
|
1055
942
|
}
|
|
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);
|
|
943
|
+
case "not": {
|
|
944
|
+
return { ...predicate, predicate: resolvePredicate(predicate.predicate, ctx) };
|
|
1067
945
|
}
|
|
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) {
|
|
946
|
+
case "when": {
|
|
1104
947
|
return {
|
|
1105
|
-
|
|
1106
|
-
|
|
948
|
+
...predicate,
|
|
949
|
+
condition: resolvePredicate(predicate.condition, ctx),
|
|
950
|
+
consequence: resolvePredicate(predicate.consequence, ctx),
|
|
951
|
+
otherwise: predicate.otherwise == null ? void 0 : resolvePredicate(predicate.otherwise, ctx)
|
|
1107
952
|
};
|
|
1108
953
|
}
|
|
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) {
|
|
954
|
+
case "and": {
|
|
1117
955
|
return {
|
|
1118
|
-
|
|
1119
|
-
|
|
956
|
+
...predicate,
|
|
957
|
+
predicates: predicate.predicates.map((p) => resolvePredicate(p, ctx))
|
|
1120
958
|
};
|
|
1121
959
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
batchError: error instanceof Error ? error.message : String(error),
|
|
1126
|
-
perItem: /* @__PURE__ */ new Map()
|
|
1127
|
-
};
|
|
960
|
+
case "count": {
|
|
961
|
+
return predicate;
|
|
962
|
+
}
|
|
1128
963
|
}
|
|
1129
964
|
}
|
|
1130
|
-
function
|
|
1131
|
-
if (
|
|
1132
|
-
return
|
|
965
|
+
function resolveAssertion(assertion, ctx) {
|
|
966
|
+
if (assertion.kind === "deleted") {
|
|
967
|
+
return assertion;
|
|
968
|
+
}
|
|
969
|
+
const descriptor = ctx.captures.get(assertion);
|
|
970
|
+
if (descriptor == null) {
|
|
971
|
+
throw new Error("internal: capture assertion was not registered");
|
|
1133
972
|
}
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
973
|
+
const as = aliasFor(ctx, descriptor);
|
|
974
|
+
return assertion.kind === "created" ? { as, kind: "created", props: setMap(assertion.props, ctx) } : { as, kind: "updated", props: updateMap(assertion.props, ctx) };
|
|
975
|
+
}
|
|
976
|
+
function updateMap(map, ctx) {
|
|
977
|
+
return Object.fromEntries(
|
|
978
|
+
Object.entries(map).map(([key2, value2]) => [
|
|
979
|
+
key2,
|
|
980
|
+
isChanged(value2) ? value2 : resolveValue(value2, ctx)
|
|
981
|
+
])
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
function whereMap(map, ctx) {
|
|
985
|
+
return Object.fromEntries(
|
|
986
|
+
Object.entries(map).map(([key2, value2]) => [
|
|
987
|
+
key2,
|
|
988
|
+
resolveWhere(value2, ctx)
|
|
989
|
+
])
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
function resolveWhere(value2, ctx) {
|
|
993
|
+
return isWithin2(value2) ? { ...value2, selection: { ...value2.selection, where: whereMap(value2.selection.where, ctx) } } : resolveValue(value2, ctx);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/test.ts
|
|
997
|
+
function test(intent, fn) {
|
|
998
|
+
const sourcePath = captureSourcePath();
|
|
999
|
+
if (fn == null) {
|
|
1000
|
+
return { spec: stubSpec(intent, sourcePath) };
|
|
1137
1001
|
}
|
|
1002
|
+
const final = finalize(fn());
|
|
1138
1003
|
return {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1004
|
+
spec: {
|
|
1005
|
+
absent: final.absent,
|
|
1006
|
+
exclusive: final.exclusive,
|
|
1007
|
+
intent,
|
|
1008
|
+
maybe: final.maybe,
|
|
1009
|
+
name: slugify(intent),
|
|
1010
|
+
params: final.params,
|
|
1011
|
+
singletons: final.singletons,
|
|
1012
|
+
sourcePath,
|
|
1013
|
+
steps: final.steps,
|
|
1014
|
+
stub: false,
|
|
1015
|
+
world: final.world
|
|
1016
|
+
}
|
|
1142
1017
|
};
|
|
1143
1018
|
}
|
|
1144
|
-
function
|
|
1019
|
+
function stubSpec(intent, sourcePath) {
|
|
1145
1020
|
return {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1021
|
+
absent: [],
|
|
1022
|
+
exclusive: [],
|
|
1023
|
+
intent,
|
|
1024
|
+
maybe: [],
|
|
1025
|
+
name: slugify(intent),
|
|
1026
|
+
params: {},
|
|
1027
|
+
singletons: {},
|
|
1028
|
+
sourcePath,
|
|
1029
|
+
steps: [],
|
|
1030
|
+
stub: true,
|
|
1031
|
+
world: []
|
|
1152
1032
|
};
|
|
1153
1033
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
if (!def.implemented) {
|
|
1160
|
-
return { error: `Observer "${name}" is not implemented`, outcome: void 0, success: false };
|
|
1034
|
+
var TESTS_ANCHOR_PATTERN = /[/\\]\.ripplo[/\\]tests[/\\]([^):]+?)(?::\d+:\d+\)?)?$/;
|
|
1035
|
+
function captureSourcePath() {
|
|
1036
|
+
const stack = new Error("capture").stack;
|
|
1037
|
+
if (stack == null) {
|
|
1038
|
+
return void 0;
|
|
1161
1039
|
}
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1040
|
+
const match = stack.split("\n").map((line) => TESTS_ANCHOR_PATTERN.exec(line)).find((m) => m != null);
|
|
1041
|
+
const captured = match?.[1];
|
|
1042
|
+
return captured == null ? void 0 : captured.replaceAll("\\", "/");
|
|
1043
|
+
}
|
|
1044
|
+
function slugify(intent) {
|
|
1045
|
+
const slug = intent.toLowerCase().replaceAll(/[^a-z0-9]+/g, " ").trim().split(" ").join("-");
|
|
1046
|
+
if (slug.length === 0) {
|
|
1047
|
+
throw new Error(`test intent "${intent}" slugifies to an empty string`);
|
|
1169
1048
|
}
|
|
1049
|
+
return slug;
|
|
1170
1050
|
}
|
|
1171
|
-
|
|
1051
|
+
|
|
1052
|
+
// src/params.ts
|
|
1053
|
+
function arbitrary(field2, example) {
|
|
1054
|
+
const token = {
|
|
1055
|
+
base: `${field2.entity}_${field2.field}`,
|
|
1056
|
+
example,
|
|
1057
|
+
valueSpace: field2.valueSpaceName
|
|
1058
|
+
};
|
|
1059
|
+
return paramBinding(token);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/engine.ts
|
|
1063
|
+
import { err, ok, okAsync, Result, ResultAsync } from "neverthrow";
|
|
1064
|
+
function createEngine(ripplo, impls, teardown) {
|
|
1065
|
+
const entities = new Map(Object.entries(impls.entities));
|
|
1066
|
+
const singletons = new Map(
|
|
1067
|
+
Object.entries(impls.singletons).map(([name, impl]) => [
|
|
1068
|
+
name,
|
|
1069
|
+
// 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
|
|
1070
|
+
impl
|
|
1071
|
+
])
|
|
1072
|
+
);
|
|
1073
|
+
assertDeclared(ripplo, [...entities.keys()], [...singletons.keys()]);
|
|
1074
|
+
const entityImpl = (name) => lookup(entities.get(name), name);
|
|
1075
|
+
const singletonImpl = (name) => lookup(singletons.get(name), name);
|
|
1172
1076
|
return {
|
|
1173
|
-
runId
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1077
|
+
read: (request, runId) => ResultAsync.combine([
|
|
1078
|
+
ResultAsync.combine(
|
|
1079
|
+
request.entities.map((name) => readEntity(entityImpl(name), name, runId))
|
|
1080
|
+
),
|
|
1081
|
+
ResultAsync.combine(
|
|
1082
|
+
request.singletons.map((name) => readSingleton(singletonImpl(name), name, runId))
|
|
1083
|
+
)
|
|
1084
|
+
]).map(([entityPairs, singletonPairs]) => ({
|
|
1085
|
+
entities: Object.fromEntries(entityPairs),
|
|
1086
|
+
singletons: Object.fromEntries(singletonPairs)
|
|
1087
|
+
})),
|
|
1088
|
+
seed: (request, runId) => ResultAsync.fromPromise(teardown(runId), implFailure).andThen(() => seedSingletons(request.singletons, singletonImpl, runId)).andThen(() => seedSetups(request.entities, entityImpl, runId)),
|
|
1089
|
+
teardown: (runId) => ResultAsync.fromPromise(teardown(runId), implFailure)
|
|
1177
1090
|
};
|
|
1178
1091
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
const order = topoOrder(
|
|
1186
|
-
defsByName,
|
|
1187
|
-
allNamesUnique.map((n) => ({ names: [n], runId: "" }))
|
|
1092
|
+
function assertDeclared(ripplo, entityNames, singletonNames) {
|
|
1093
|
+
const backendEntities = new Set(
|
|
1094
|
+
ripplo.entities.filter((e) => e.schema.source === "backend").map((e) => e.name)
|
|
1095
|
+
);
|
|
1096
|
+
const backendSingletons = new Set(
|
|
1097
|
+
ripplo.singletons.filter((s) => s.schema.source === "backend").map((s) => s.name)
|
|
1188
1098
|
);
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1099
|
+
entityNames.forEach((name) => {
|
|
1100
|
+
if (!backendEntities.has(name)) {
|
|
1101
|
+
throw new Error(`engine impl "${name}" has no matching backend entity`);
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
singletonNames.forEach((name) => {
|
|
1105
|
+
if (!backendSingletons.has(name)) {
|
|
1106
|
+
throw new Error(`engine impl "${name}" has no matching backend singleton`);
|
|
1195
1107
|
}
|
|
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
1108
|
});
|
|
1202
1109
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1110
|
+
function lookup(impl, name) {
|
|
1111
|
+
return impl == null ? err({ message: `no engine impl for "${name}"` }) : ok(impl);
|
|
1112
|
+
}
|
|
1113
|
+
function readEntity(impl, name, runId) {
|
|
1114
|
+
return impl.asyncAndThen(
|
|
1115
|
+
(i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((rows) => [
|
|
1116
|
+
name,
|
|
1117
|
+
[...rows]
|
|
1118
|
+
])
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
function implFailure(error) {
|
|
1122
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
1123
|
+
}
|
|
1124
|
+
function readSingleton(impl, name, runId) {
|
|
1125
|
+
return impl.asyncAndThen(
|
|
1126
|
+
(i) => ResultAsync.fromPromise(i.read({ runId }), implFailure).map((value2) => [
|
|
1127
|
+
name,
|
|
1128
|
+
value2
|
|
1129
|
+
])
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
function seedSingletons(values, singletonImpl, runId) {
|
|
1133
|
+
return ResultAsync.combine(
|
|
1134
|
+
Object.entries(values).map(
|
|
1135
|
+
([name, value2]) => singletonImpl(name).asyncAndThen(
|
|
1136
|
+
(impl) => ResultAsync.fromPromise(impl.seed({ runId, value: value2 }), implFailure)
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
function seedSetups(specs, entityImpl, runId) {
|
|
1142
|
+
const waves = dependencyWaves(specs);
|
|
1143
|
+
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));
|
|
1144
|
+
}
|
|
1145
|
+
function dependencyWaves(specs) {
|
|
1146
|
+
return buildWaves([], specs);
|
|
1147
|
+
}
|
|
1148
|
+
function buildWaves(done, remaining) {
|
|
1149
|
+
if (remaining.length === 0) {
|
|
1150
|
+
return done;
|
|
1212
1151
|
}
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1152
|
+
const seeded = new Set(done.flat().map((spec) => spec.as));
|
|
1153
|
+
const ready = remaining.filter((spec) => refAliases(spec).every((alias) => seeded.has(alias)));
|
|
1154
|
+
if (ready.length === 0) {
|
|
1155
|
+
return [...done, remaining];
|
|
1216
1156
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1157
|
+
return buildWaves(
|
|
1158
|
+
[...done, ready],
|
|
1159
|
+
remaining.filter((spec) => !ready.includes(spec))
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
function refAliases(spec) {
|
|
1163
|
+
return Object.values(spec.fields).flatMap((value2) => {
|
|
1164
|
+
if (value2 === null || typeof value2 !== "object" || !("ref" in value2)) {
|
|
1165
|
+
return [];
|
|
1166
|
+
}
|
|
1167
|
+
const lastDot = value2.ref.lastIndexOf(".");
|
|
1168
|
+
return lastDot === -1 ? [] : [value2.ref.slice(0, lastDot)];
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
function seedWave(wave, entityImpl, acc, runId) {
|
|
1172
|
+
return ResultAsync.combine(
|
|
1173
|
+
wave.map((spec) => seedSetup(entityImpl(spec.entity), acc, spec, runId))
|
|
1174
|
+
).map((folds) => mergeFolds(acc, folds));
|
|
1175
|
+
}
|
|
1176
|
+
function seedSetup(impl, acc, spec, runId) {
|
|
1177
|
+
return resolveFields(spec.fields, acc.env).asyncAndThen((fields) => runSeed(impl, fields, runId)).map(({ row: row2, session }) => foldRow(acc, spec, row2, session));
|
|
1178
|
+
}
|
|
1179
|
+
function resolveFields(fields, env) {
|
|
1180
|
+
return Result.combine(
|
|
1181
|
+
Object.entries(fields).map(
|
|
1182
|
+
([field2, value2]) => resolveValue2(value2, env).map((cell2) => [field2, cell2])
|
|
1183
|
+
)
|
|
1184
|
+
).map((entries) => Object.fromEntries(entries));
|
|
1185
|
+
}
|
|
1186
|
+
function resolveValue2(value2, env) {
|
|
1187
|
+
if (value2 === null || typeof value2 !== "object") {
|
|
1188
|
+
return ok(value2);
|
|
1229
1189
|
}
|
|
1190
|
+
if ("ref" in value2) {
|
|
1191
|
+
return env.has(value2.ref) ? ok(env.get(value2.ref) ?? null) : err({ message: `setup ref "${value2.ref}" was not produced by an earlier entity` });
|
|
1192
|
+
}
|
|
1193
|
+
throw new Error("internal: a setup value resolved to an unresolved template");
|
|
1194
|
+
}
|
|
1195
|
+
function runSeed(impl, fields, runId) {
|
|
1196
|
+
return impl.asyncAndThen((i) => ResultAsync.fromPromise(i.seed({ fields, runId }), implFailure));
|
|
1197
|
+
}
|
|
1198
|
+
function foldRow(acc, spec, row2, session) {
|
|
1199
|
+
const next = Object.entries(row2).map(([field2, value2]) => [
|
|
1200
|
+
`${spec.as}.${field2}`,
|
|
1201
|
+
value2
|
|
1202
|
+
]);
|
|
1203
|
+
return {
|
|
1204
|
+
env: new Map([...acc.env, ...next]),
|
|
1205
|
+
rows: [...acc.rows, { as: spec.as, row: row2, session }]
|
|
1206
|
+
};
|
|
1230
1207
|
}
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1208
|
+
function mergeFolds(base, folds) {
|
|
1209
|
+
return {
|
|
1210
|
+
env: new Map([...base.env, ...folds.flatMap((fold) => [...fold.env])]),
|
|
1211
|
+
rows: [...base.rows, ...folds.flatMap((fold) => fold.rows.slice(base.rows.length))]
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function orderRows(specs, rows) {
|
|
1215
|
+
const byAs = new Map(rows.map((row2) => [row2.as, row2]));
|
|
1216
|
+
return specs.flatMap((spec) => {
|
|
1217
|
+
const row2 = byAs.get(spec.as);
|
|
1218
|
+
return row2 == null ? [] : [row2];
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/client-engine.ts
|
|
1223
|
+
import { z as z8 } from "zod";
|
|
1224
|
+
|
|
1225
|
+
// ../spec/src/leaves.ts
|
|
1226
|
+
import { z } from "zod";
|
|
1227
|
+
var budgetSchema = z.enum(["fast", "slow", "async"]);
|
|
1228
|
+
var valueRefSchema = z.object({ ref: z.string().min(1) });
|
|
1229
|
+
var primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
1230
|
+
var templateSchema = z.object({
|
|
1231
|
+
template: z.array(z.union([z.string(), valueRefSchema])).min(1)
|
|
1232
|
+
});
|
|
1233
|
+
var setValueSchema = z.union([valueRefSchema, primitiveSchema, templateSchema, z.null()]);
|
|
1234
|
+
var changedSchema = z.object({ kind: z.literal("changed") });
|
|
1235
|
+
var updateValueSchema = z.union([setValueSchema, changedSchema]);
|
|
1236
|
+
var stringValueSchema = z.union([z.string(), valueRefSchema, templateSchema]);
|
|
1237
|
+
var roleLocatorSchema = z.object({
|
|
1238
|
+
by: z.literal("role"),
|
|
1239
|
+
name: z.union([stringValueSchema, z.undefined()]).optional().transform((value2) => value2),
|
|
1240
|
+
role: z.string().min(1)
|
|
1241
|
+
});
|
|
1242
|
+
var testIdLocatorSchema = z.object({ by: z.literal("testId"), value: stringValueSchema });
|
|
1243
|
+
var insideLocatorSchema = z.object({
|
|
1244
|
+
by: z.literal("inside"),
|
|
1245
|
+
scope: z.lazy(() => locatorSchema),
|
|
1246
|
+
target: z.lazy(() => locatorSchema)
|
|
1247
|
+
});
|
|
1248
|
+
var locatorSchema = z.discriminatedUnion("by", [
|
|
1249
|
+
roleLocatorSchema,
|
|
1250
|
+
testIdLocatorSchema,
|
|
1251
|
+
insideLocatorSchema
|
|
1252
|
+
]);
|
|
1253
|
+
var primitiveTypeSchema = z.enum(["string", "number", "boolean"]);
|
|
1254
|
+
var consistencyClassSchema = z.enum(["strict", "eventual"]);
|
|
1255
|
+
var stringConstraintsSchema = z.object({
|
|
1256
|
+
kind: z.literal("string"),
|
|
1257
|
+
maxLength: z.number().int().positive().optional(),
|
|
1258
|
+
minLength: z.number().int().nonnegative().optional(),
|
|
1259
|
+
pattern: z.string().optional()
|
|
1260
|
+
});
|
|
1261
|
+
var numberConstraintsSchema = z.object({
|
|
1262
|
+
kind: z.literal("number"),
|
|
1263
|
+
max: z.number().int().optional(),
|
|
1264
|
+
min: z.number().int().optional()
|
|
1265
|
+
});
|
|
1266
|
+
var datetimeConstraintsSchema = z.object({
|
|
1267
|
+
kind: z.literal("datetime"),
|
|
1268
|
+
maxOffsetDays: z.number().int(),
|
|
1269
|
+
minOffsetDays: z.number().int()
|
|
1270
|
+
});
|
|
1271
|
+
var constraintsSchema = z.discriminatedUnion("kind", [
|
|
1272
|
+
stringConstraintsSchema,
|
|
1273
|
+
numberConstraintsSchema,
|
|
1274
|
+
datetimeConstraintsSchema
|
|
1275
|
+
]);
|
|
1276
|
+
var generatorSchema = z.enum([
|
|
1277
|
+
"company.name",
|
|
1278
|
+
"date.iso",
|
|
1279
|
+
"internet.email",
|
|
1280
|
+
"internet.url",
|
|
1281
|
+
"person.fullName",
|
|
1282
|
+
"lorem.slug",
|
|
1283
|
+
"lorem.word"
|
|
1284
|
+
]);
|
|
1285
|
+
var valueSpaceSchema = z.object({
|
|
1286
|
+
constraints: constraintsSchema.optional(),
|
|
1287
|
+
generator: generatorSchema,
|
|
1288
|
+
name: z.string().min(1),
|
|
1289
|
+
type: primitiveTypeSchema,
|
|
1290
|
+
values: z.array(primitiveSchema).min(1).optional()
|
|
1291
|
+
});
|
|
1292
|
+
var propSpecSchema = z.object({
|
|
1293
|
+
consistency: consistencyClassSchema.default("strict"),
|
|
1294
|
+
optional: z.boolean(),
|
|
1295
|
+
stable: z.boolean(),
|
|
1296
|
+
type: primitiveTypeSchema,
|
|
1297
|
+
valueSpace: z.string().min(1).optional()
|
|
1298
|
+
});
|
|
1299
|
+
var sourceSchema = z.enum(["backend", "client"]);
|
|
1300
|
+
var entitySchemaSchema = z.object({
|
|
1301
|
+
description: z.string().optional(),
|
|
1302
|
+
identity: z.array(z.string().min(1)).min(1),
|
|
1303
|
+
identityKind: z.enum(["surrogate", "natural"]),
|
|
1304
|
+
name: z.string().min(1),
|
|
1305
|
+
props: z.record(z.string().min(1), propSpecSchema),
|
|
1306
|
+
source: sourceSchema.default("backend")
|
|
1307
|
+
});
|
|
1308
|
+
var singletonSchemaSchema = z.object({
|
|
1309
|
+
consistency: consistencyClassSchema.default("strict"),
|
|
1310
|
+
default: primitiveSchema,
|
|
1311
|
+
description: z.string().optional(),
|
|
1312
|
+
name: z.string().min(1),
|
|
1313
|
+
source: sourceSchema.default("backend"),
|
|
1314
|
+
type: primitiveTypeSchema,
|
|
1315
|
+
valueSpace: z.string().min(1).optional()
|
|
1316
|
+
});
|
|
1317
|
+
var browserSingletonSchema = z.enum(["url", "title", "viewport"]);
|
|
1318
|
+
|
|
1319
|
+
// ../spec/src/predicate.ts
|
|
1320
|
+
import { z as z2 } from "zod";
|
|
1321
|
+
var stateAssertionSchema = z2.discriminatedUnion("kind", [
|
|
1322
|
+
z2.object({
|
|
1323
|
+
as: z2.string().min(1),
|
|
1324
|
+
kind: z2.literal("created"),
|
|
1325
|
+
props: z2.record(z2.string().min(1), setValueSchema)
|
|
1326
|
+
}),
|
|
1327
|
+
z2.object({
|
|
1328
|
+
as: z2.string().min(1),
|
|
1329
|
+
kind: z2.literal("updated"),
|
|
1330
|
+
props: z2.record(z2.string().min(1), updateValueSchema)
|
|
1331
|
+
}),
|
|
1332
|
+
z2.object({ kind: z2.literal("deleted") })
|
|
1333
|
+
]);
|
|
1334
|
+
var singletonAssertionSchema = z2.object({ kind: z2.literal("is"), value: setValueSchema });
|
|
1335
|
+
var whereValueSchema = z2.lazy(
|
|
1336
|
+
() => z2.union([setValueSchema, withinSchema])
|
|
1337
|
+
);
|
|
1338
|
+
var selectionSchema = z2.object({
|
|
1339
|
+
entity: z2.string().min(1),
|
|
1340
|
+
where: z2.record(z2.string().min(1), whereValueSchema)
|
|
1341
|
+
});
|
|
1342
|
+
var withinSchema = z2.object({
|
|
1343
|
+
field: z2.string().min(1),
|
|
1344
|
+
kind: z2.literal("within"),
|
|
1345
|
+
selection: selectionSchema
|
|
1346
|
+
});
|
|
1347
|
+
var wait = z2.union([budgetSchema, z2.undefined()]).optional().transform((value2) => value2);
|
|
1348
|
+
var predicateSchema = z2.lazy(
|
|
1349
|
+
() => z2.discriminatedUnion("kind", [
|
|
1350
|
+
z2.object({ kind: z2.literal("visible"), locator: locatorSchema, wait }),
|
|
1351
|
+
z2.object({ kind: z2.literal("disabled"), locator: locatorSchema, wait }),
|
|
1352
|
+
z2.object({ kind: z2.literal("enabled"), locator: locatorSchema, wait }),
|
|
1353
|
+
z2.object({ kind: z2.literal("focused"), locator: locatorSchema, wait }),
|
|
1354
|
+
z2.object({ kind: z2.literal("value"), locator: locatorSchema, value: stringValueSchema, wait }),
|
|
1355
|
+
z2.object({ kind: z2.literal("text"), locator: locatorSchema, value: stringValueSchema, wait }),
|
|
1356
|
+
z2.object({
|
|
1357
|
+
assertion: singletonAssertionSchema,
|
|
1358
|
+
kind: z2.literal("singleton"),
|
|
1359
|
+
singleton: z2.string().min(1),
|
|
1360
|
+
wait
|
|
1361
|
+
}),
|
|
1362
|
+
z2.object({
|
|
1363
|
+
kind: z2.literal("browser"),
|
|
1364
|
+
name: browserSingletonSchema,
|
|
1365
|
+
value: stringValueSchema,
|
|
1366
|
+
wait
|
|
1367
|
+
}),
|
|
1368
|
+
z2.object({
|
|
1369
|
+
assertion: stateAssertionSchema,
|
|
1370
|
+
entity: z2.string().min(1),
|
|
1371
|
+
key: z2.record(z2.string().min(1), whereValueSchema),
|
|
1372
|
+
kind: z2.literal("state"),
|
|
1373
|
+
wait
|
|
1374
|
+
}),
|
|
1375
|
+
z2.object({ kind: z2.literal("not"), predicate: predicateSchema }),
|
|
1376
|
+
z2.object({ kind: z2.literal("and"), predicates: z2.array(predicateSchema) }),
|
|
1377
|
+
z2.object({
|
|
1378
|
+
entity: z2.string().min(1),
|
|
1379
|
+
kind: z2.literal("count"),
|
|
1380
|
+
value: z2.number().int().nonnegative()
|
|
1381
|
+
}),
|
|
1382
|
+
z2.object({
|
|
1383
|
+
condition: predicateSchema,
|
|
1384
|
+
consequence: predicateSchema,
|
|
1385
|
+
kind: z2.literal("when"),
|
|
1386
|
+
otherwise: z2.union([predicateSchema, z2.undefined()]).optional().transform((value2) => value2)
|
|
1387
|
+
})
|
|
1388
|
+
])
|
|
1389
|
+
);
|
|
1390
|
+
|
|
1391
|
+
// ../spec/src/codec.ts
|
|
1392
|
+
import { z as z3 } from "zod";
|
|
1393
|
+
var envelopeSchema = z3.object({
|
|
1394
|
+
__codec: z3.string().min(1),
|
|
1395
|
+
data: z3.unknown(),
|
|
1396
|
+
version: z3.number().int().positive()
|
|
1397
|
+
});
|
|
1398
|
+
var CodecVersionError = class extends Error {
|
|
1399
|
+
codec;
|
|
1400
|
+
currentVersion;
|
|
1401
|
+
gotVersion;
|
|
1402
|
+
constructor(params) {
|
|
1403
|
+
super(
|
|
1404
|
+
`Unsupported ${params.codec} version ${String(params.gotVersion)} (current ${String(params.currentVersion)}). Upgrade Ripplo or rebuild with a compatible CLI.`
|
|
1405
|
+
);
|
|
1406
|
+
this.name = "CodecVersionError";
|
|
1407
|
+
this.codec = params.codec;
|
|
1408
|
+
this.currentVersion = params.currentVersion;
|
|
1409
|
+
this.gotVersion = params.gotVersion;
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
var CodecMismatchError = class extends Error {
|
|
1413
|
+
constructor(params) {
|
|
1414
|
+
super(`Codec mismatch: expected "${params.expected}", got "${params.got}"`);
|
|
1415
|
+
this.name = "CodecMismatchError";
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
function defineCodec({
|
|
1419
|
+
name,
|
|
1420
|
+
schema
|
|
1235
1421
|
}) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1422
|
+
return {
|
|
1423
|
+
currentVersion: 1,
|
|
1424
|
+
name,
|
|
1425
|
+
decode: (raw) => decode({ name, raw, schema }),
|
|
1426
|
+
encode: (value2) => ({
|
|
1427
|
+
__codec: name,
|
|
1428
|
+
data: value2,
|
|
1429
|
+
version: 1
|
|
1430
|
+
})
|
|
1240
1431
|
};
|
|
1432
|
+
}
|
|
1433
|
+
function decode({
|
|
1434
|
+
name,
|
|
1435
|
+
raw,
|
|
1436
|
+
schema
|
|
1437
|
+
}) {
|
|
1438
|
+
const envelope = envelopeSchema.parse(raw);
|
|
1439
|
+
if (envelope.__codec !== name) {
|
|
1440
|
+
throw new CodecMismatchError({ expected: name, got: envelope.__codec });
|
|
1441
|
+
}
|
|
1442
|
+
if (envelope.version !== 1) {
|
|
1443
|
+
throw new CodecVersionError({ codec: name, currentVersion: 1, gotVersion: envelope.version });
|
|
1444
|
+
}
|
|
1445
|
+
return schema.parse(envelope.data);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ../spec/src/client-channel.ts
|
|
1449
|
+
var CLIENT_MOUNT_KEY = "__ripplo__";
|
|
1450
|
+
var CLIENT_SEED_KEY = "__ripplo_seed__";
|
|
1451
|
+
|
|
1452
|
+
// ../spec/src/lockfile.ts
|
|
1453
|
+
import { z as z4 } from "zod";
|
|
1454
|
+
var actionSchema = z4.discriminatedUnion("kind", [
|
|
1455
|
+
z4.object({ kind: z4.literal("goto"), url: stringValueSchema }),
|
|
1456
|
+
z4.object({ kind: z4.literal("fill"), locator: locatorSchema, value: setValueSchema }),
|
|
1457
|
+
z4.object({ kind: z4.literal("clear"), locator: locatorSchema }),
|
|
1458
|
+
z4.object({ kind: z4.literal("click"), locator: locatorSchema }),
|
|
1459
|
+
z4.object({ kind: z4.literal("dblclick"), locator: locatorSchema }),
|
|
1460
|
+
z4.object({ kind: z4.literal("select"), locator: locatorSchema, value: setValueSchema }),
|
|
1461
|
+
z4.object({ kind: z4.literal("check"), locator: locatorSchema }),
|
|
1462
|
+
z4.object({ kind: z4.literal("uncheck"), locator: locatorSchema }),
|
|
1463
|
+
z4.object({ kind: z4.literal("hover"), locator: locatorSchema }),
|
|
1464
|
+
z4.object({
|
|
1465
|
+
files: z4.array(z4.string().min(1)).min(1),
|
|
1466
|
+
kind: z4.literal("upload"),
|
|
1467
|
+
locator: locatorSchema
|
|
1468
|
+
}),
|
|
1469
|
+
z4.object({ key: z4.string().min(1), kind: z4.literal("press"), locator: locatorSchema.optional() })
|
|
1470
|
+
]);
|
|
1471
|
+
var stepSchema = z4.object({
|
|
1472
|
+
action: actionSchema,
|
|
1473
|
+
expect: z4.array(predicateSchema).default([])
|
|
1474
|
+
});
|
|
1475
|
+
var paramSchema = z4.object({
|
|
1476
|
+
example: primitiveSchema.optional(),
|
|
1477
|
+
valueSpace: z4.string().min(1)
|
|
1478
|
+
});
|
|
1479
|
+
var setupSchema = z4.object({
|
|
1480
|
+
as: z4.string().min(1),
|
|
1481
|
+
entity: z4.string().min(1),
|
|
1482
|
+
set: z4.record(z4.string().min(1), setValueSchema)
|
|
1483
|
+
});
|
|
1484
|
+
var absenceSchema = z4.object({
|
|
1485
|
+
entity: z4.string().min(1),
|
|
1486
|
+
where: z4.record(z4.string().min(1), setValueSchema)
|
|
1487
|
+
});
|
|
1488
|
+
var testSchema = z4.object({
|
|
1489
|
+
absent: z4.array(absenceSchema).default([]),
|
|
1490
|
+
exclusive: z4.array(z4.string().min(1)).default([]),
|
|
1491
|
+
intent: z4.string().min(1),
|
|
1492
|
+
maybe: z4.array(setupSchema).default([]),
|
|
1493
|
+
name: z4.string().min(1),
|
|
1494
|
+
params: z4.record(z4.string().min(1), paramSchema),
|
|
1495
|
+
singletons: z4.record(z4.string().min(1), setValueSchema).default({}),
|
|
1496
|
+
sourcePath: z4.string().min(1).optional(),
|
|
1497
|
+
steps: z4.array(stepSchema).default([]),
|
|
1498
|
+
stub: z4.boolean().default(false),
|
|
1499
|
+
world: z4.array(setupSchema).default([])
|
|
1500
|
+
});
|
|
1501
|
+
var fixtureEntrySchema = z4.object({
|
|
1502
|
+
sha256: z4.string().regex(/^[0-9a-f]{64}$/u),
|
|
1503
|
+
size: z4.number().int().nonnegative()
|
|
1504
|
+
});
|
|
1505
|
+
var lockfileSchema = z4.object({
|
|
1506
|
+
entities: z4.array(entitySchemaSchema),
|
|
1507
|
+
fixtures: z4.record(z4.string().min(1), fixtureEntrySchema).default({}),
|
|
1508
|
+
singletons: z4.array(singletonSchemaSchema).default([]),
|
|
1509
|
+
tests: z4.array(testSchema),
|
|
1510
|
+
valueSpaces: z4.array(valueSpaceSchema)
|
|
1511
|
+
});
|
|
1512
|
+
var lockfileCodec = defineCodec({ name: "ripplo-lockfile", schema: lockfileSchema });
|
|
1513
|
+
|
|
1514
|
+
// ../spec/src/sync-payload.ts
|
|
1515
|
+
import { z as z5 } from "zod";
|
|
1516
|
+
var stepDescriptorSchema = z5.object({
|
|
1517
|
+
index: z5.number().int().nonnegative(),
|
|
1518
|
+
kind: z5.string(),
|
|
1519
|
+
target: z5.string(),
|
|
1520
|
+
value: z5.string()
|
|
1521
|
+
});
|
|
1522
|
+
var workflowSpecSchema = z5.object({
|
|
1523
|
+
steps: z5.array(stepDescriptorSchema),
|
|
1524
|
+
stub: z5.boolean().default(false)
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// ../spec/src/session.ts
|
|
1528
|
+
import { z as z6 } from "zod";
|
|
1529
|
+
var sameSiteSchema = z6.enum(["Strict", "Lax", "None"]);
|
|
1530
|
+
var cookieSchema = z6.object({
|
|
1531
|
+
domain: z6.string().min(1),
|
|
1532
|
+
expires: z6.number(),
|
|
1533
|
+
httpOnly: z6.boolean(),
|
|
1534
|
+
name: z6.string().min(1),
|
|
1535
|
+
path: z6.string().min(1),
|
|
1536
|
+
sameSite: sameSiteSchema,
|
|
1537
|
+
secure: z6.boolean(),
|
|
1538
|
+
value: z6.string()
|
|
1539
|
+
});
|
|
1540
|
+
var originSchema = z6.object({
|
|
1541
|
+
localStorage: z6.array(z6.object({ name: z6.string().min(1), value: z6.string() })),
|
|
1542
|
+
origin: z6.string().min(1)
|
|
1543
|
+
});
|
|
1544
|
+
var sessionSchema = z6.object({
|
|
1545
|
+
cookies: z6.array(cookieSchema),
|
|
1546
|
+
headers: z6.record(z6.string().min(1), z6.string()).optional(),
|
|
1547
|
+
origins: z6.array(originSchema)
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// ../spec/src/engine.ts
|
|
1551
|
+
import { z as z7 } from "zod";
|
|
1552
|
+
var cellSchema = z7.union([primitiveSchema, z7.null()]);
|
|
1553
|
+
var rowSchema = z7.record(z7.string().min(1), cellSchema);
|
|
1554
|
+
var setupSpecSchema = z7.object({
|
|
1555
|
+
as: z7.string().min(1),
|
|
1556
|
+
entity: z7.string().min(1),
|
|
1557
|
+
fields: z7.record(z7.string().min(1), setValueSchema)
|
|
1558
|
+
});
|
|
1559
|
+
var setupRequestSchema = z7.object({
|
|
1560
|
+
entities: z7.array(setupSpecSchema),
|
|
1561
|
+
runId: z7.string().min(1),
|
|
1562
|
+
singletons: z7.record(z7.string().min(1), cellSchema).default({})
|
|
1563
|
+
});
|
|
1564
|
+
var setupRowSchema = z7.object({
|
|
1565
|
+
as: z7.string().min(1),
|
|
1566
|
+
row: rowSchema,
|
|
1567
|
+
session: sessionSchema.optional()
|
|
1568
|
+
});
|
|
1569
|
+
var setupResponseSchema = z7.object({
|
|
1570
|
+
rows: z7.array(setupRowSchema)
|
|
1571
|
+
});
|
|
1572
|
+
var stateRequestSchema = z7.object({
|
|
1573
|
+
entities: z7.array(z7.string().min(1)),
|
|
1574
|
+
runId: z7.string().min(1),
|
|
1575
|
+
singletons: z7.array(z7.string().min(1)).default([])
|
|
1576
|
+
});
|
|
1577
|
+
var stateResponseSchema = z7.object({
|
|
1578
|
+
entities: z7.record(z7.string().min(1), z7.array(rowSchema)),
|
|
1579
|
+
singletons: z7.record(z7.string().min(1), cellSchema).default({})
|
|
1580
|
+
});
|
|
1581
|
+
var teardownRequestSchema = z7.object({
|
|
1582
|
+
runId: z7.string().min(1)
|
|
1583
|
+
});
|
|
1584
|
+
var teardownResponseSchema = z7.object({
|
|
1585
|
+
ok: z7.literal(true)
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// src/client-engine.ts
|
|
1589
|
+
var seedSchema = z8.record(z8.string(), z8.union([z8.string(), z8.number(), z8.boolean()])).catch({});
|
|
1590
|
+
function createClientEngine(_ripplo, impls) {
|
|
1591
|
+
const singletons = new Map(
|
|
1592
|
+
Object.entries(impls.singletons).map(([name, impl]) => [
|
|
1593
|
+
name,
|
|
1594
|
+
// 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
|
|
1595
|
+
impl
|
|
1596
|
+
])
|
|
1597
|
+
);
|
|
1598
|
+
const entities = new Map(Object.entries(impls.entities));
|
|
1599
|
+
const seed = seedSchema.parse(Reflect.get(globalThis, CLIENT_SEED_KEY));
|
|
1600
|
+
Object.entries(seed).forEach(([name, value2]) => singletons.get(name)?.seed(value2));
|
|
1241
1601
|
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()}`)
|
|
1602
|
+
readEntity: (name) => entities.get(name)?.read() ?? [],
|
|
1603
|
+
readSingleton: (name) => singletons.get(name)?.read() ?? null
|
|
1250
1604
|
};
|
|
1251
1605
|
}
|
|
1252
|
-
function
|
|
1253
|
-
if (
|
|
1254
|
-
return
|
|
1606
|
+
function mountClientEngine(ripplo, impls, { enabled: enabled2 }) {
|
|
1607
|
+
if (!enabled2) {
|
|
1608
|
+
return;
|
|
1255
1609
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1610
|
+
Reflect.set(globalThis, CLIENT_MOUNT_KEY, createClientEngine(ripplo, impls));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/build.ts
|
|
1614
|
+
function buildLockfile(input) {
|
|
1615
|
+
assertUniqueNames(input.entities);
|
|
1616
|
+
const lockfile = lockfileSchema.parse({
|
|
1617
|
+
entities: input.entities.map((handle) => handle.schema),
|
|
1618
|
+
singletons: input.singletons.map((handle) => handle.schema),
|
|
1619
|
+
tests: input.tests.map((ripploTest) => ripploTest.spec),
|
|
1620
|
+
valueSpaces: dedupeByName([
|
|
1621
|
+
...input.entities.flatMap((handle) => handle.valueSpaces),
|
|
1622
|
+
...input.singletons.flatMap((handle) => handle.valueSpaces)
|
|
1623
|
+
])
|
|
1624
|
+
});
|
|
1625
|
+
assertNoContradictions(lockfile);
|
|
1626
|
+
return lockfile;
|
|
1627
|
+
}
|
|
1628
|
+
function assertUniqueNames(entities) {
|
|
1629
|
+
const names = entities.map((handle) => handle.schema.name);
|
|
1630
|
+
const duplicate = names.find((name, index) => names.indexOf(name) !== index);
|
|
1631
|
+
if (duplicate != null) {
|
|
1632
|
+
throw new Error(`duplicate entity name "${duplicate}" \u2014 each entity name must be unique`);
|
|
1260
1633
|
}
|
|
1261
1634
|
}
|
|
1635
|
+
function dedupeByName(spaces) {
|
|
1636
|
+
const byName = new Map(spaces.map((space) => [space.name, space]));
|
|
1637
|
+
return [...byName.values()];
|
|
1638
|
+
}
|
|
1639
|
+
function assertNoContradictions(lockfile) {
|
|
1640
|
+
lockfile.tests.forEach((test2) => {
|
|
1641
|
+
assertNoContradiction(test2);
|
|
1642
|
+
assertNoDanglingRefs(test2);
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
function assertNoContradiction(test2) {
|
|
1646
|
+
const setups = [...test2.world, ...test2.maybe];
|
|
1647
|
+
test2.absent.forEach((absence) => {
|
|
1648
|
+
const clash = setups.find(
|
|
1649
|
+
(setup) => setup.entity === absence.entity && whereMatches(absence.where, setup.set)
|
|
1650
|
+
);
|
|
1651
|
+
if (clash != null) {
|
|
1652
|
+
throw new Error(
|
|
1653
|
+
`test "${test2.name}": creates a "${absence.entity}" ("${clash.as}") that a none(${absence.entity}, \u2026) in its world forbids`
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
function whereMatches(where, set) {
|
|
1659
|
+
return Object.entries(where).every(([field2, want]) => sameValue(want, set[field2]));
|
|
1660
|
+
}
|
|
1661
|
+
function sameValue(a, b) {
|
|
1662
|
+
return b !== void 0 && sameSetValue(a, b);
|
|
1663
|
+
}
|
|
1664
|
+
function assertNoDanglingRefs(test2) {
|
|
1665
|
+
const aliases = new Set([...test2.world, ...test2.maybe].map((setup) => setup.as));
|
|
1666
|
+
const paramKeys = new Set(Object.keys(test2.params));
|
|
1667
|
+
const fieldSets = [
|
|
1668
|
+
...test2.world.map((setup) => setup.set),
|
|
1669
|
+
...test2.maybe.map((setup) => setup.set),
|
|
1670
|
+
...test2.absent.map((absence) => absence.where)
|
|
1671
|
+
];
|
|
1672
|
+
fieldSets.forEach((set) => {
|
|
1673
|
+
assertSetRefs(test2.name, set, aliases, paramKeys);
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
function assertSetRefs(testName, set, aliases, paramKeys) {
|
|
1677
|
+
Object.values(set).forEach((value2) => {
|
|
1678
|
+
if (!isRef(value2) || paramKeys.has(value2.ref)) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const lastDot = value2.ref.lastIndexOf(".");
|
|
1682
|
+
if (lastDot === -1) {
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const aliasPath = value2.ref.slice(0, lastDot);
|
|
1686
|
+
if (!aliases.has(aliasPath)) {
|
|
1687
|
+
throw new Error(
|
|
1688
|
+
`test "${testName}": ref "${value2.ref}" points at unknown alias "${aliasPath}"`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
function isRef(value2) {
|
|
1694
|
+
return value2 != null && typeof value2 === "object" && "ref" in value2;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/ripplo.ts
|
|
1698
|
+
function createRipplo(input) {
|
|
1699
|
+
return {
|
|
1700
|
+
entities: input.entities,
|
|
1701
|
+
lockfile: buildLockfile({
|
|
1702
|
+
entities: input.entities,
|
|
1703
|
+
singletons: input.singletons,
|
|
1704
|
+
tests: input.tests
|
|
1705
|
+
}),
|
|
1706
|
+
singletons: input.singletons,
|
|
1707
|
+
tests: input.tests
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1262
1710
|
export {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1711
|
+
CLIENT_MOUNT_KEY,
|
|
1712
|
+
CLIENT_SEED_KEY,
|
|
1713
|
+
alert,
|
|
1714
|
+
and,
|
|
1715
|
+
arbitrary,
|
|
1716
|
+
banner,
|
|
1717
|
+
button,
|
|
1718
|
+
cell,
|
|
1719
|
+
changed,
|
|
1720
|
+
check,
|
|
1721
|
+
checkbox,
|
|
1722
|
+
clear,
|
|
1723
|
+
click,
|
|
1724
|
+
combobox,
|
|
1725
|
+
complementary,
|
|
1726
|
+
contentinfo,
|
|
1727
|
+
count,
|
|
1728
|
+
createClientEngine,
|
|
1267
1729
|
createEngine,
|
|
1268
1730
|
createRipplo,
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1731
|
+
dblclick,
|
|
1732
|
+
dialog,
|
|
1733
|
+
disabled,
|
|
1734
|
+
enabled,
|
|
1735
|
+
entity,
|
|
1736
|
+
field,
|
|
1737
|
+
fill,
|
|
1738
|
+
focused,
|
|
1739
|
+
form,
|
|
1740
|
+
goto,
|
|
1741
|
+
heading,
|
|
1742
|
+
hover,
|
|
1743
|
+
id,
|
|
1744
|
+
img,
|
|
1745
|
+
inside,
|
|
1746
|
+
key,
|
|
1747
|
+
link,
|
|
1748
|
+
list,
|
|
1749
|
+
listitem,
|
|
1750
|
+
main,
|
|
1751
|
+
menu,
|
|
1752
|
+
menuitem,
|
|
1753
|
+
mountClientEngine,
|
|
1754
|
+
navigation,
|
|
1755
|
+
not,
|
|
1756
|
+
option,
|
|
1757
|
+
press,
|
|
1758
|
+
radio,
|
|
1759
|
+
region,
|
|
1760
|
+
role,
|
|
1761
|
+
row,
|
|
1762
|
+
searchbox,
|
|
1763
|
+
select,
|
|
1764
|
+
singleton,
|
|
1765
|
+
status,
|
|
1766
|
+
tab,
|
|
1767
|
+
table,
|
|
1768
|
+
tablist,
|
|
1275
1769
|
test,
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1770
|
+
testId,
|
|
1771
|
+
text,
|
|
1772
|
+
textbox,
|
|
1773
|
+
title,
|
|
1774
|
+
uncheck,
|
|
1775
|
+
upload,
|
|
1776
|
+
url,
|
|
1777
|
+
v,
|
|
1778
|
+
value,
|
|
1779
|
+
viewport,
|
|
1780
|
+
visible,
|
|
1781
|
+
when,
|
|
1782
|
+
within
|
|
1279
1783
|
};
|