@ripplo/testing 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -172
- package/dist/assert.d.ts +1 -1
- package/dist/assert.js +4 -4
- package/dist/{builder-c7tXey03.d.ts → builder-dqXTFZ4j.d.ts} +29 -29
- package/dist/{chunk-3IL457A7.js → chunk-P4ZI7G5M.js} +22 -2
- package/dist/chunk-TO3T2D2Y.js +84 -0
- package/dist/compiler.d.ts +2 -2
- package/dist/engine-CphCJ1ZS.d.ts +47 -0
- package/dist/express.d.ts +5 -4
- package/dist/express.js +4 -7
- package/dist/fastify.d.ts +5 -4
- package/dist/fastify.js +4 -7
- package/dist/index.d.ts +5 -28
- package/dist/index.js +522 -270
- package/dist/lockfile.d.ts +2 -2
- package/dist/nextjs.d.ts +5 -4
- package/dist/nextjs.js +6 -9
- package/dist/{types-oYS_Yv4G.d.ts → types-yIhY8cwG.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-CD3M7H5A.js +0 -332
package/dist/index.js
CHANGED
|
@@ -1,218 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_IGNORE_PATHS,
|
|
3
|
+
DEFAULT_WATCH_PATHS,
|
|
4
|
+
createTestValue,
|
|
5
|
+
dslConfigSchema,
|
|
6
|
+
makeObserverHandle,
|
|
7
|
+
readObserverBudget,
|
|
8
|
+
readObserverDescription,
|
|
9
|
+
readObserverName,
|
|
10
|
+
readPreconditionDepMapping,
|
|
11
|
+
readPreconditionDependsOn,
|
|
12
|
+
readPreconditionDescription,
|
|
13
|
+
readPreconditionName,
|
|
14
|
+
readTestValue
|
|
15
|
+
} from "./chunk-P4ZI7G5M.js";
|
|
1
16
|
import {
|
|
2
17
|
compile
|
|
3
18
|
} from "./chunk-KNF4K4JH.js";
|
|
4
19
|
import "./chunk-MGATMMCZ.js";
|
|
5
20
|
import {
|
|
6
|
-
buildObserver,
|
|
7
21
|
buildSetCookieHeader,
|
|
8
|
-
createEngine,
|
|
9
22
|
serializeCookie,
|
|
10
23
|
verifyWebhookSignature
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import {
|
|
13
|
-
DEFAULT_IGNORE_PATHS,
|
|
14
|
-
DEFAULT_WATCH_PATHS,
|
|
15
|
-
dslConfigSchema,
|
|
16
|
-
readObserverName,
|
|
17
|
-
readPreconditionName
|
|
18
|
-
} from "./chunk-3IL457A7.js";
|
|
24
|
+
} from "./chunk-TO3T2D2Y.js";
|
|
19
25
|
|
|
20
|
-
// src/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
|
|
38
|
-
}
|
|
39
|
-
const mapping = existing.depMapping;
|
|
40
|
-
preconditions[idx] = {
|
|
41
|
-
...existing,
|
|
42
|
-
implemented: true,
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TData narrows Record<string, string>, safe at engine boundary
|
|
44
|
-
teardown: impl.teardown,
|
|
45
|
-
setup: async (ctx, allDeps) => {
|
|
46
|
-
const resolved = {};
|
|
47
|
-
mapping.forEach(([key, depName]) => {
|
|
48
|
-
const data = allDeps[depName];
|
|
49
|
-
if (data != null) {
|
|
50
|
-
resolved[key] = data;
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
return impl.setup(ctx, resolved);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function implementObserver(handle, impl) {
|
|
58
|
-
const name = readObserverName(handle);
|
|
59
|
-
const idx = observers.findIndex((o) => o.name === name);
|
|
60
|
-
if (idx === -1) {
|
|
61
|
-
throw new Error(`Cannot implement unknown observer: "${name}"`);
|
|
62
|
-
}
|
|
63
|
-
const existing = observers[idx];
|
|
64
|
-
if (existing == null) {
|
|
65
|
-
throw new Error(`Cannot implement unknown observer: "${name}"`);
|
|
66
|
-
}
|
|
67
|
-
observers[idx] = {
|
|
68
|
-
...existing,
|
|
69
|
-
implemented: true,
|
|
70
|
-
run: async (ctx, params) => {
|
|
71
|
-
return impl(ctx, params);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
implementObserver,
|
|
77
|
-
implementPrecondition,
|
|
78
|
-
getConfig: () => config,
|
|
79
|
-
getObservers: () => observers,
|
|
80
|
-
getPreconditions: () => preconditions,
|
|
81
|
-
getTests: () => tests,
|
|
82
|
-
getUnimplemented() {
|
|
83
|
-
return {
|
|
84
|
-
observers: observers.filter((o) => !o.implemented).map((o) => o.name),
|
|
85
|
-
preconditions: preconditions.filter((p) => !p.implemented).map((p) => p.name),
|
|
86
|
-
tests: tests.filter((t) => !t.implemented).map((t) => t.name)
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
observer(name) {
|
|
90
|
-
if (observerNames.has(name)) {
|
|
91
|
-
observers.splice(0, observers.length, ...observers.filter((o) => o.name !== name));
|
|
92
|
-
}
|
|
93
|
-
observerNames.add(name);
|
|
94
|
-
return buildObserver({ name, observers });
|
|
26
|
+
// src/observer.ts
|
|
27
|
+
function createPassOutcome() {
|
|
28
|
+
return { kind: "pass" };
|
|
29
|
+
}
|
|
30
|
+
function createRetryOutcome(reason) {
|
|
31
|
+
return { kind: "retry", reason };
|
|
32
|
+
}
|
|
33
|
+
function createFailOutcome(reason) {
|
|
34
|
+
return { kind: "fail", reason };
|
|
35
|
+
}
|
|
36
|
+
function buildObserver(name) {
|
|
37
|
+
let description = "";
|
|
38
|
+
let budget = "fast";
|
|
39
|
+
const self = {
|
|
40
|
+
budget(tier) {
|
|
41
|
+
budget = tier;
|
|
42
|
+
return self;
|
|
95
43
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
0,
|
|
100
|
-
preconditions.length,
|
|
101
|
-
...preconditions.filter((p) => p.name !== name)
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
preconditionNames.add(name);
|
|
105
|
-
return buildPrecondition(name, preconditions);
|
|
44
|
+
description(text) {
|
|
45
|
+
description = text;
|
|
46
|
+
return self;
|
|
106
47
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
testNames.add(id);
|
|
113
|
-
return buildTestName(id, tests, options?.uiOnly);
|
|
48
|
+
input() {
|
|
49
|
+
return {
|
|
50
|
+
contract: () => makeObserverHandle({ budget, description, name })
|
|
51
|
+
};
|
|
114
52
|
}
|
|
115
53
|
};
|
|
54
|
+
return self;
|
|
116
55
|
}
|
|
117
|
-
|
|
118
|
-
|
|
56
|
+
|
|
57
|
+
// src/builder.ts
|
|
58
|
+
function precondition(name) {
|
|
59
|
+
return buildPreconditionStart(name);
|
|
119
60
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
mapping.forEach(([key, depName]) => {
|
|
123
|
-
const data = allDeps[depName];
|
|
124
|
-
if (data != null) {
|
|
125
|
-
resolved[key] = data;
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
return resolved;
|
|
61
|
+
function observer(name) {
|
|
62
|
+
return buildObserver(name);
|
|
129
63
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
64
|
+
function test(id, options) {
|
|
65
|
+
validateTestId(id);
|
|
66
|
+
return buildTestName(id, options?.uiOnly);
|
|
132
67
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
68
|
+
function createRipplo(rawConfig, registries) {
|
|
69
|
+
const config = dslConfigSchema.parse(rawConfig);
|
|
70
|
+
const { observers, preconditions, tests } = registries;
|
|
71
|
+
validateUniqueNames(preconditions, observers, tests);
|
|
72
|
+
const preconditionDefs = Object.values(preconditions).map((p) => stubPreconditionDef(p));
|
|
73
|
+
const observerDefs = Object.values(observers).map((o) => stubObserverDef(o));
|
|
74
|
+
const testDefs = [...tests];
|
|
75
|
+
return {
|
|
76
|
+
config,
|
|
77
|
+
observers,
|
|
78
|
+
preconditions,
|
|
79
|
+
tests: testDefs,
|
|
80
|
+
getConfig: () => config,
|
|
81
|
+
getObservers: () => observerDefs,
|
|
82
|
+
getPreconditions: () => preconditionDefs,
|
|
83
|
+
getTests: () => testDefs,
|
|
84
|
+
getUnimplemented: () => ({
|
|
85
|
+
observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
|
|
86
|
+
preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
|
|
87
|
+
tests: testDefs.filter((t) => !t.implemented).map((t) => t.id)
|
|
88
|
+
})
|
|
89
|
+
};
|
|
135
90
|
}
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
91
|
+
function stubPreconditionDef(p) {
|
|
92
|
+
const name = readPreconditionName(p);
|
|
93
|
+
return {
|
|
94
|
+
dependsOn: readPreconditionDependsOn(p),
|
|
95
|
+
depMapping: readPreconditionDepMapping(p),
|
|
96
|
+
description: readPreconditionDescription(p),
|
|
141
97
|
implemented: false,
|
|
142
|
-
name
|
|
98
|
+
name,
|
|
143
99
|
returns: [],
|
|
144
100
|
teardown: void 0,
|
|
145
101
|
setup: () => Promise.resolve({})
|
|
146
|
-
}
|
|
147
|
-
return makePrecondition(params.name);
|
|
102
|
+
};
|
|
148
103
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
setup: castBuilder(setupNoDepsImpl),
|
|
158
|
-
description(text) {
|
|
159
|
-
description = text;
|
|
160
|
-
return self;
|
|
161
|
-
},
|
|
162
|
-
notImplemented() {
|
|
163
|
-
pushStub({ depMapping: [], description, name, preconditions });
|
|
164
|
-
return makePrecondition(name);
|
|
165
|
-
}
|
|
104
|
+
function stubObserverDef(o) {
|
|
105
|
+
const name = readObserverName(o);
|
|
106
|
+
return {
|
|
107
|
+
budget: readObserverBudget(o),
|
|
108
|
+
description: readObserverDescription(o),
|
|
109
|
+
implemented: false,
|
|
110
|
+
name,
|
|
111
|
+
run: () => Promise.resolve(createFailOutcome(`observer "${name}" not implemented`))
|
|
166
112
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
),
|
|
175
|
-
setup: castBuilder(setupWithDepsImpl),
|
|
176
|
-
description(text) {
|
|
177
|
-
description = text;
|
|
178
|
-
return withDeps;
|
|
179
|
-
},
|
|
180
|
-
notImplemented() {
|
|
181
|
-
pushStub({ depMapping: mapping, description, name, preconditions });
|
|
182
|
-
return makePrecondition(name);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
return castBuilder(withDeps);
|
|
186
|
-
function setupWithDepsImpl(fn) {
|
|
187
|
-
return registerPrecondition(deps, fn);
|
|
113
|
+
}
|
|
114
|
+
function validateUniqueNames(preconditions, observers, tests) {
|
|
115
|
+
const pNames = /* @__PURE__ */ new Set();
|
|
116
|
+
Object.values(preconditions).forEach((p) => {
|
|
117
|
+
const name = readPreconditionName(p);
|
|
118
|
+
if (pNames.has(name)) {
|
|
119
|
+
throw new Error(`Duplicate precondition name: "${name}"`);
|
|
188
120
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
preconditions.push(def);
|
|
209
|
-
return {
|
|
210
|
-
teardown(tdFn) {
|
|
211
|
-
def.teardown = tdFn;
|
|
212
|
-
return makePrecondition(name);
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
}
|
|
121
|
+
pNames.add(name);
|
|
122
|
+
});
|
|
123
|
+
const oNames = /* @__PURE__ */ new Set();
|
|
124
|
+
Object.values(observers).forEach((o) => {
|
|
125
|
+
const name = readObserverName(o);
|
|
126
|
+
if (oNames.has(name)) {
|
|
127
|
+
throw new Error(`Duplicate observer name: "${name}"`);
|
|
128
|
+
}
|
|
129
|
+
oNames.add(name);
|
|
130
|
+
});
|
|
131
|
+
const tIds = /* @__PURE__ */ new Set();
|
|
132
|
+
tests.forEach((t) => {
|
|
133
|
+
if (tIds.has(t.id)) {
|
|
134
|
+
throw new Error(`Duplicate test id: "${t.id}"`);
|
|
135
|
+
}
|
|
136
|
+
tIds.add(t.id);
|
|
137
|
+
});
|
|
216
138
|
}
|
|
217
139
|
var TEST_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
218
140
|
function validateTestId(id) {
|
|
@@ -222,135 +144,189 @@ function validateTestId(id) {
|
|
|
222
144
|
);
|
|
223
145
|
}
|
|
224
146
|
}
|
|
225
|
-
function
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
147
|
+
function makePrecondition(params) {
|
|
148
|
+
return params;
|
|
149
|
+
}
|
|
150
|
+
function buildDepMapping(deps) {
|
|
151
|
+
return Object.entries(deps).map(([key, dep]) => [key, readPreconditionName(dep)]);
|
|
152
|
+
}
|
|
153
|
+
function buildPreconditionStart(name) {
|
|
154
|
+
let description = "";
|
|
155
|
+
const self = {
|
|
156
|
+
contract: () => makePrecondition({
|
|
157
|
+
dependsOn: [],
|
|
158
|
+
depMapping: [],
|
|
159
|
+
description,
|
|
160
|
+
name
|
|
161
|
+
}),
|
|
162
|
+
description(text) {
|
|
163
|
+
description = text;
|
|
164
|
+
return self;
|
|
165
|
+
},
|
|
166
|
+
requires(deps) {
|
|
167
|
+
return buildPreconditionWithDeps({ deps, name, getDescription: () => description });
|
|
229
168
|
}
|
|
230
169
|
};
|
|
170
|
+
return self;
|
|
231
171
|
}
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
uiOnly
|
|
172
|
+
function buildPreconditionWithDeps({
|
|
173
|
+
deps,
|
|
174
|
+
getDescription,
|
|
175
|
+
name
|
|
237
176
|
}) {
|
|
177
|
+
const description = getDescription();
|
|
178
|
+
const depMapping = buildDepMapping(deps);
|
|
179
|
+
const dependsOn = depMapping.map(([, depName]) => depName);
|
|
180
|
+
return {
|
|
181
|
+
contract: () => makePrecondition({
|
|
182
|
+
dependsOn,
|
|
183
|
+
depMapping,
|
|
184
|
+
description,
|
|
185
|
+
name
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function buildTestName(id, uiOnly) {
|
|
190
|
+
return {
|
|
191
|
+
name: (displayName) => buildTestRequires({ id, name: displayName, uiOnly })
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function castOutcome(value) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
function buildTestRequires({ id, name, uiOnly }) {
|
|
238
198
|
let description = "";
|
|
239
199
|
const self = {
|
|
240
|
-
requires: castBuilder(requiresImpl),
|
|
241
200
|
description(text) {
|
|
242
201
|
description = text;
|
|
243
202
|
return self;
|
|
203
|
+
},
|
|
204
|
+
requires(reqs) {
|
|
205
|
+
const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
|
|
206
|
+
const requiresKeys = {};
|
|
207
|
+
Object.entries(reqs).forEach(([key, p]) => {
|
|
208
|
+
requiresKeys[key] = readPreconditionName(p);
|
|
209
|
+
});
|
|
210
|
+
return castOutcome(
|
|
211
|
+
buildTestOutcome({
|
|
212
|
+
id,
|
|
213
|
+
name,
|
|
214
|
+
reqNames,
|
|
215
|
+
requiresKeys,
|
|
216
|
+
uiOnly,
|
|
217
|
+
getDescription: () => description
|
|
218
|
+
})
|
|
219
|
+
);
|
|
244
220
|
}
|
|
245
221
|
};
|
|
246
222
|
return self;
|
|
247
|
-
function requiresImpl(reqs) {
|
|
248
|
-
const reqNames = Object.values(reqs).map((r) => readPreconditionName(r));
|
|
249
|
-
const requiresKeys = {};
|
|
250
|
-
Object.entries(reqs).forEach(([key, precondition]) => {
|
|
251
|
-
requiresKeys[key] = readPreconditionName(precondition);
|
|
252
|
-
});
|
|
253
|
-
return buildOutcome({ description, id, name, reqNames, requiresKeys, tests, uiOnly });
|
|
254
|
-
}
|
|
255
223
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
224
|
+
function buildTestOutcome({
|
|
225
|
+
getDescription,
|
|
258
226
|
id,
|
|
259
227
|
name,
|
|
260
228
|
reqNames,
|
|
261
229
|
requiresKeys,
|
|
262
|
-
tests,
|
|
263
230
|
uiOnly
|
|
264
231
|
}) {
|
|
265
|
-
|
|
232
|
+
const description = getDescription();
|
|
266
233
|
return {
|
|
267
|
-
description(text) {
|
|
268
|
-
description = text;
|
|
269
|
-
return this;
|
|
270
|
-
},
|
|
271
234
|
expectedOutcome(text) {
|
|
272
|
-
return
|
|
235
|
+
return buildTestStartsAt({
|
|
273
236
|
description,
|
|
274
237
|
expectedOutcome: text,
|
|
275
238
|
id,
|
|
276
239
|
name,
|
|
277
240
|
reqNames,
|
|
278
241
|
requiresKeys,
|
|
279
|
-
tests,
|
|
280
242
|
uiOnly
|
|
281
243
|
});
|
|
282
244
|
}
|
|
283
245
|
};
|
|
284
246
|
}
|
|
285
|
-
function
|
|
247
|
+
function buildTestStartsAt({
|
|
286
248
|
description,
|
|
287
249
|
expectedOutcome,
|
|
288
250
|
id,
|
|
289
251
|
name,
|
|
290
252
|
reqNames,
|
|
291
253
|
requiresKeys,
|
|
292
|
-
tests,
|
|
293
254
|
uiOnly
|
|
294
255
|
}) {
|
|
295
256
|
return {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
function startsAtImpl(fn) {
|
|
313
|
-
return {
|
|
314
|
-
steps: castBuilder(stepsImpl)
|
|
315
|
-
};
|
|
316
|
-
function stepsImpl(stepsFn) {
|
|
317
|
-
tests.push({
|
|
257
|
+
notImplemented: () => ({
|
|
258
|
+
description,
|
|
259
|
+
expectedOutcome,
|
|
260
|
+
id,
|
|
261
|
+
implemented: false,
|
|
262
|
+
name,
|
|
263
|
+
requires: [...reqNames],
|
|
264
|
+
requiresKeys,
|
|
265
|
+
startsAtFn: void 0,
|
|
266
|
+
stepsFn: void 0,
|
|
267
|
+
uiOnly
|
|
268
|
+
}),
|
|
269
|
+
startsAt(fn) {
|
|
270
|
+
return buildTestSteps({
|
|
318
271
|
description,
|
|
319
272
|
expectedOutcome,
|
|
320
273
|
id,
|
|
321
|
-
implemented: true,
|
|
322
274
|
name,
|
|
323
|
-
|
|
275
|
+
reqNames,
|
|
324
276
|
requiresKeys,
|
|
325
277
|
startsAtFn: fn,
|
|
326
|
-
stepsFn,
|
|
327
278
|
uiOnly
|
|
328
279
|
});
|
|
329
280
|
}
|
|
330
|
-
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function buildTestSteps({
|
|
284
|
+
description,
|
|
285
|
+
expectedOutcome,
|
|
286
|
+
id,
|
|
287
|
+
name,
|
|
288
|
+
reqNames,
|
|
289
|
+
requiresKeys,
|
|
290
|
+
startsAtFn,
|
|
291
|
+
uiOnly
|
|
292
|
+
}) {
|
|
293
|
+
return {
|
|
294
|
+
steps: (stepsFn) => ({
|
|
295
|
+
description,
|
|
296
|
+
expectedOutcome,
|
|
297
|
+
id,
|
|
298
|
+
implemented: true,
|
|
299
|
+
name,
|
|
300
|
+
requires: [...reqNames],
|
|
301
|
+
requiresKeys,
|
|
302
|
+
startsAtFn,
|
|
303
|
+
stepsFn,
|
|
304
|
+
uiOnly
|
|
305
|
+
})
|
|
306
|
+
};
|
|
331
307
|
}
|
|
332
308
|
|
|
333
309
|
// src/lint.ts
|
|
334
310
|
function lint(result) {
|
|
335
311
|
const diagnostics = [];
|
|
336
|
-
result.tests.forEach((
|
|
337
|
-
const nodes = getOrderedNodes(
|
|
312
|
+
result.tests.forEach((test2) => {
|
|
313
|
+
const nodes = getOrderedNodes(test2);
|
|
338
314
|
const report = (diagnostic) => {
|
|
339
|
-
diagnostics.push({ ...diagnostic, test:
|
|
315
|
+
diagnostics.push({ ...diagnostic, test: test2.slug });
|
|
340
316
|
};
|
|
341
317
|
RULES.forEach((rule) => {
|
|
342
|
-
rule(nodes,
|
|
318
|
+
rule(nodes, test2, report);
|
|
343
319
|
});
|
|
344
320
|
});
|
|
345
321
|
return { diagnostics };
|
|
346
322
|
}
|
|
347
|
-
function getOrderedNodes(
|
|
323
|
+
function getOrderedNodes(test2) {
|
|
348
324
|
const result = [];
|
|
349
|
-
let currentId =
|
|
325
|
+
let currentId = test2.spec.entryNode;
|
|
350
326
|
const visited = /* @__PURE__ */ new Set();
|
|
351
327
|
while (currentId != null && !visited.has(currentId)) {
|
|
352
328
|
visited.add(currentId);
|
|
353
|
-
const found =
|
|
329
|
+
const found = test2.spec.nodes[currentId];
|
|
354
330
|
if (found == null) {
|
|
355
331
|
break;
|
|
356
332
|
}
|
|
@@ -370,8 +346,8 @@ function exactTextMatch(nodes, _test, report) {
|
|
|
370
346
|
}
|
|
371
347
|
});
|
|
372
348
|
}
|
|
373
|
-
function noHardcodedData(nodes,
|
|
374
|
-
const hasVariables = Object.keys(
|
|
349
|
+
function noHardcodedData(nodes, test2, report) {
|
|
350
|
+
const hasVariables = Object.keys(test2.spec.variables ?? {}).length > 0;
|
|
375
351
|
if (!hasVariables) {
|
|
376
352
|
return;
|
|
377
353
|
}
|
|
@@ -388,8 +364,8 @@ function noHardcodedData(nodes, test, report) {
|
|
|
388
364
|
}
|
|
389
365
|
});
|
|
390
366
|
}
|
|
391
|
-
function preferPreconditionData(nodes,
|
|
392
|
-
const variableKeys = Object.keys(
|
|
367
|
+
function preferPreconditionData(nodes, test2, report) {
|
|
368
|
+
const variableKeys = Object.keys(test2.spec.variables ?? {});
|
|
393
369
|
if (variableKeys.length === 0) {
|
|
394
370
|
return;
|
|
395
371
|
}
|
|
@@ -461,8 +437,8 @@ function assertMatchesOutcome(nodes, _test, report) {
|
|
|
461
437
|
});
|
|
462
438
|
}
|
|
463
439
|
}
|
|
464
|
-
function noEmptySteps(nodes,
|
|
465
|
-
if (!
|
|
440
|
+
function noEmptySteps(nodes, test2, report) {
|
|
441
|
+
if (!test2.implemented) {
|
|
466
442
|
return;
|
|
467
443
|
}
|
|
468
444
|
if (nodes.length === 0) {
|
|
@@ -509,8 +485,8 @@ var EFFECT_ASSERTION_TYPES = /* @__PURE__ */ new Set([
|
|
|
509
485
|
function isEffectAssertion(node) {
|
|
510
486
|
return EFFECT_ASSERTION_TYPES.has(node.type);
|
|
511
487
|
}
|
|
512
|
-
function noAssertions(nodes,
|
|
513
|
-
if (!
|
|
488
|
+
function noAssertions(nodes, test2, report) {
|
|
489
|
+
if (!test2.implemented || nodes.length === 0) {
|
|
514
490
|
return;
|
|
515
491
|
}
|
|
516
492
|
if (!nodes.some((n) => isAssertionNode(n))) {
|
|
@@ -521,8 +497,8 @@ function noAssertions(nodes, test, report) {
|
|
|
521
497
|
});
|
|
522
498
|
}
|
|
523
499
|
}
|
|
524
|
-
function lowAssertionRatio(nodes,
|
|
525
|
-
if (!
|
|
500
|
+
function lowAssertionRatio(nodes, test2, report) {
|
|
501
|
+
if (!test2.implemented || nodes.length <= 3) {
|
|
526
502
|
return;
|
|
527
503
|
}
|
|
528
504
|
const assertions = nodes.filter((n) => isAssertionNode(n)).length;
|
|
@@ -646,11 +622,11 @@ function nodeKeywords(node) {
|
|
|
646
622
|
const locName = loc.by === "role" ? loc.name ?? "" : loc.value;
|
|
647
623
|
return [...fromLabel, ...tokenize(locName)];
|
|
648
624
|
}
|
|
649
|
-
function expectedOutcomeKeywordCoverage(nodes,
|
|
650
|
-
if (!
|
|
625
|
+
function expectedOutcomeKeywordCoverage(nodes, test2, report) {
|
|
626
|
+
if (!test2.implemented || nodes.length === 0) {
|
|
651
627
|
return;
|
|
652
628
|
}
|
|
653
|
-
const outcomeTokens = new Set(tokenize(
|
|
629
|
+
const outcomeTokens = new Set(tokenize(test2.expectedOutcome));
|
|
654
630
|
if (outcomeTokens.size === 0) {
|
|
655
631
|
return;
|
|
656
632
|
}
|
|
@@ -684,8 +660,8 @@ function isLikelyBackendMutation(node) {
|
|
|
684
660
|
const name = loc.by === "role" ? loc.name ?? "" : loc.value;
|
|
685
661
|
return BACKEND_MUTATION_KEYWORDS.test(name);
|
|
686
662
|
}
|
|
687
|
-
function mutationWithoutObserverCoverage(nodes,
|
|
688
|
-
if (!
|
|
663
|
+
function mutationWithoutObserverCoverage(nodes, test2, report) {
|
|
664
|
+
if (!test2.implemented || test2.spec.uiOnly === true) {
|
|
689
665
|
return;
|
|
690
666
|
}
|
|
691
667
|
nodes.forEach((node, index) => {
|
|
@@ -710,8 +686,8 @@ function mutationWithoutObserverCoverage(nodes, test, report) {
|
|
|
710
686
|
});
|
|
711
687
|
});
|
|
712
688
|
}
|
|
713
|
-
function observerParamsReferenceVariables(nodes,
|
|
714
|
-
const variableKeys = Object.keys(
|
|
689
|
+
function observerParamsReferenceVariables(nodes, test2, report) {
|
|
690
|
+
const variableKeys = Object.keys(test2.spec.variables ?? {});
|
|
715
691
|
if (variableKeys.length === 0) {
|
|
716
692
|
return;
|
|
717
693
|
}
|
|
@@ -752,6 +728,278 @@ var RULES = [
|
|
|
752
728
|
mutationWithoutObserverCoverage,
|
|
753
729
|
observerParamsReferenceVariables
|
|
754
730
|
];
|
|
731
|
+
|
|
732
|
+
// src/engine.ts
|
|
733
|
+
function notImplemented(reason) {
|
|
734
|
+
return { reason: reason ?? "not implemented" };
|
|
735
|
+
}
|
|
736
|
+
function isNotImplemented(value) {
|
|
737
|
+
return typeof value === "object" && value !== null && "reason" in value && Object.keys(value).length === 1;
|
|
738
|
+
}
|
|
739
|
+
function createEngine(ripplo, impls) {
|
|
740
|
+
const preconditionDefs = wirePreconditions(ripplo.preconditions, impls.preconditions);
|
|
741
|
+
const observerDefs = wireObservers(ripplo.observers, impls.observers);
|
|
742
|
+
const preconditionsByName = new Map(preconditionDefs.map((d) => [d.name, d]));
|
|
743
|
+
const observersByName = new Map(observerDefs.map((d) => [d.name, d]));
|
|
744
|
+
return {
|
|
745
|
+
executeObserver: (name, params) => executeObserver(observersByName, name, params),
|
|
746
|
+
executePreconditions: (names, options) => executePreconditions({ defsByName: preconditionsByName, names, options }),
|
|
747
|
+
getConfig: () => ripplo.getConfig(),
|
|
748
|
+
getObservers: () => observerDefs,
|
|
749
|
+
getPreconditions: () => preconditionDefs,
|
|
750
|
+
getUnimplemented: () => ({
|
|
751
|
+
observers: observerDefs.filter((o) => !o.implemented).map((o) => o.name),
|
|
752
|
+
preconditions: preconditionDefs.filter((p) => !p.implemented).map((p) => p.name),
|
|
753
|
+
tests: ripplo.tests.filter((t) => !t.implemented).map((t) => t.id)
|
|
754
|
+
}),
|
|
755
|
+
teardown: (names, data) => teardown(preconditionsByName, names, data)
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function wirePreconditions(registry, impls) {
|
|
759
|
+
return Object.entries(registry).map(([key, handle]) => {
|
|
760
|
+
const impl = Reflect.get(impls, key);
|
|
761
|
+
if (isNotImplemented(impl)) {
|
|
762
|
+
return makeNotImplementedPreconditionDef(handle, impl);
|
|
763
|
+
}
|
|
764
|
+
return makeWiredPreconditionDef(handle, impl);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function wireObservers(registry, impls) {
|
|
768
|
+
return Object.entries(registry).map(([key, handle]) => {
|
|
769
|
+
const impl = Reflect.get(impls, key);
|
|
770
|
+
if (isNotImplemented(impl)) {
|
|
771
|
+
return makeNotImplementedObserverDef(handle, impl);
|
|
772
|
+
}
|
|
773
|
+
return makeWiredObserverDef(handle, impl);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function makeNotImplementedPreconditionDef(handle, sentinel) {
|
|
777
|
+
const name = readPreconditionName(handle);
|
|
778
|
+
return {
|
|
779
|
+
dependsOn: readPreconditionDepMappingKeys(handle),
|
|
780
|
+
depMapping: readPreconditionDepMapping(handle),
|
|
781
|
+
description: readDescriptionOf(handle),
|
|
782
|
+
implemented: false,
|
|
783
|
+
name,
|
|
784
|
+
returns: [],
|
|
785
|
+
teardown: void 0,
|
|
786
|
+
setup: () => Promise.reject(new Error(`Precondition "${name}" is ${sentinel.reason}`))
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
function makeWiredPreconditionDef(handle, impl) {
|
|
790
|
+
const name = readPreconditionName(handle);
|
|
791
|
+
const mapping = readPreconditionDepMapping(handle);
|
|
792
|
+
const typedImpl = impl;
|
|
793
|
+
return {
|
|
794
|
+
dependsOn: mapping.map(([, depName]) => depName),
|
|
795
|
+
depMapping: mapping,
|
|
796
|
+
description: readDescriptionOf(handle),
|
|
797
|
+
implemented: true,
|
|
798
|
+
name,
|
|
799
|
+
returns: [],
|
|
800
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- typedImpl.teardown has narrower TData, wider context at engine boundary
|
|
801
|
+
teardown: typedImpl.teardown,
|
|
802
|
+
setup: async (ctx, allDeps) => {
|
|
803
|
+
const resolved = {};
|
|
804
|
+
mapping.forEach(([key, depName]) => {
|
|
805
|
+
const data = allDeps[depName];
|
|
806
|
+
if (data != null) {
|
|
807
|
+
resolved[key] = data;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return typedImpl.setup(ctx, resolved);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function makeNotImplementedObserverDef(handle, sentinel) {
|
|
815
|
+
const name = readObserverName(handle);
|
|
816
|
+
return {
|
|
817
|
+
budget: readBudgetOf(handle),
|
|
818
|
+
description: readDescriptionOfObserver(handle),
|
|
819
|
+
implemented: false,
|
|
820
|
+
name,
|
|
821
|
+
run: () => Promise.resolve(createFailOutcome(`observer "${name}" is ${sentinel.reason}`))
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function makeWiredObserverDef(handle, impl) {
|
|
825
|
+
const name = readObserverName(handle);
|
|
826
|
+
const typedImpl = impl;
|
|
827
|
+
return {
|
|
828
|
+
budget: readBudgetOf(handle),
|
|
829
|
+
description: readDescriptionOfObserver(handle),
|
|
830
|
+
implemented: true,
|
|
831
|
+
name,
|
|
832
|
+
run: async (ctx, params) => {
|
|
833
|
+
return typedImpl(ctx, params);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function readPreconditionDepMappingKeys(handle) {
|
|
838
|
+
return readPreconditionDepMapping(handle).map(([, depName]) => depName);
|
|
839
|
+
}
|
|
840
|
+
function readDescriptionOf(handle) {
|
|
841
|
+
return handle.description;
|
|
842
|
+
}
|
|
843
|
+
function readDescriptionOfObserver(handle) {
|
|
844
|
+
return handle.description;
|
|
845
|
+
}
|
|
846
|
+
function readBudgetOf(handle) {
|
|
847
|
+
return handle.budget;
|
|
848
|
+
}
|
|
849
|
+
async function executePreconditions({
|
|
850
|
+
defsByName,
|
|
851
|
+
names,
|
|
852
|
+
options
|
|
853
|
+
}) {
|
|
854
|
+
const runId = crypto.randomUUID().slice(0, 12);
|
|
855
|
+
const cookies = [];
|
|
856
|
+
const defaultDomain = deriveDefaultDomain(options?.appUrl);
|
|
857
|
+
const state = {
|
|
858
|
+
cookies,
|
|
859
|
+
ctx: createSetupContext({ cookies, defaultDomain, runId }),
|
|
860
|
+
data: {},
|
|
861
|
+
defsByName,
|
|
862
|
+
executed: [],
|
|
863
|
+
runId
|
|
864
|
+
};
|
|
865
|
+
return runBatchSequence(state, names);
|
|
866
|
+
}
|
|
867
|
+
async function runBatchSequence(state, names) {
|
|
868
|
+
let index = 0;
|
|
869
|
+
while (index < names.length) {
|
|
870
|
+
const name = names[index];
|
|
871
|
+
if (name == null) {
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
const error = validatePrecondition(state.defsByName, name);
|
|
875
|
+
if (error != null) {
|
|
876
|
+
return fail(state, error);
|
|
877
|
+
}
|
|
878
|
+
const stepError = await executeOnePrecondition(state, name);
|
|
879
|
+
if (stepError != null) {
|
|
880
|
+
return fail(state, stepError);
|
|
881
|
+
}
|
|
882
|
+
index += 1;
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
cookies: state.cookies,
|
|
886
|
+
data: state.data,
|
|
887
|
+
error: void 0,
|
|
888
|
+
executed: state.executed,
|
|
889
|
+
runId: state.runId,
|
|
890
|
+
success: true
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function validatePrecondition(defsByName, name) {
|
|
894
|
+
const def = defsByName.get(name);
|
|
895
|
+
if (def == null) {
|
|
896
|
+
return `Unknown precondition: "${name}"`;
|
|
897
|
+
}
|
|
898
|
+
if (!def.implemented) {
|
|
899
|
+
return `Precondition "${name}" is not implemented`;
|
|
900
|
+
}
|
|
901
|
+
return void 0;
|
|
902
|
+
}
|
|
903
|
+
async function executeOnePrecondition(state, name) {
|
|
904
|
+
const def = state.defsByName.get(name);
|
|
905
|
+
if (def == null) {
|
|
906
|
+
return `Unknown precondition: "${name}"`;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const result = await def.setup(state.ctx, state.data);
|
|
910
|
+
const resolved = {};
|
|
911
|
+
Object.entries(result).forEach(([key, value]) => {
|
|
912
|
+
resolved[key] = readTestValue(value);
|
|
913
|
+
});
|
|
914
|
+
state.data[name] = resolved;
|
|
915
|
+
state.executed.push(name);
|
|
916
|
+
return void 0;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
return error instanceof Error ? error.message : String(error);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function fail(state, error) {
|
|
922
|
+
return {
|
|
923
|
+
cookies: state.cookies,
|
|
924
|
+
data: state.data,
|
|
925
|
+
error,
|
|
926
|
+
executed: state.executed,
|
|
927
|
+
runId: state.runId,
|
|
928
|
+
success: false
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function executeObserver(observersByName, name, params) {
|
|
932
|
+
const def = observersByName.get(name);
|
|
933
|
+
if (def == null) {
|
|
934
|
+
return { error: `Unknown observer: "${name}"`, outcome: void 0, success: false };
|
|
935
|
+
}
|
|
936
|
+
if (!def.implemented) {
|
|
937
|
+
return { error: `Observer "${name}" is not implemented`, outcome: void 0, success: false };
|
|
938
|
+
}
|
|
939
|
+
const ctx = createObserverContext(crypto.randomUUID().slice(0, 12));
|
|
940
|
+
try {
|
|
941
|
+
const outcome = await def.run(ctx, params);
|
|
942
|
+
return { error: void 0, outcome, success: true };
|
|
943
|
+
} catch (error) {
|
|
944
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
945
|
+
return { error: void 0, outcome: createFailOutcome(message), success: true };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function createObserverContext(runId) {
|
|
949
|
+
return {
|
|
950
|
+
runId,
|
|
951
|
+
fail: (reason) => createFailOutcome(reason),
|
|
952
|
+
pass: () => createPassOutcome(),
|
|
953
|
+
retry: (reason) => createRetryOutcome(reason)
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
async function teardown(defsByName, names, data) {
|
|
957
|
+
const reversed = [...names].toReversed();
|
|
958
|
+
let index = 0;
|
|
959
|
+
while (index < reversed.length) {
|
|
960
|
+
const name = reversed[index];
|
|
961
|
+
if (name != null) {
|
|
962
|
+
await teardownOne(defsByName, name, data);
|
|
963
|
+
}
|
|
964
|
+
index += 1;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async function teardownOne(defsByName, name, data) {
|
|
968
|
+
const def = defsByName.get(name);
|
|
969
|
+
if (def?.teardown == null) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
await def.teardown({ data: data[name] ?? {} });
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function createSetupContext({
|
|
978
|
+
cookies,
|
|
979
|
+
defaultDomain,
|
|
980
|
+
runId
|
|
981
|
+
}) {
|
|
982
|
+
return {
|
|
983
|
+
runId,
|
|
984
|
+
fixed: (value) => createTestValue(value),
|
|
985
|
+
setCookie: (name, value, options) => {
|
|
986
|
+
const resolvedOptions = options != null && options.domain == null && defaultDomain != null ? { ...options, domain: defaultDomain } : options ?? void 0;
|
|
987
|
+
cookies.push({ name, options: resolvedOptions, value });
|
|
988
|
+
},
|
|
989
|
+
uniqueEmail: () => createTestValue(`ripplo-test-${runId}@test.ripplo.ai`),
|
|
990
|
+
uniqueId: (prefix) => createTestValue(`ripplo-test-${prefix}-${runId}`)
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function deriveDefaultDomain(baseUrl) {
|
|
994
|
+
if (baseUrl == null) {
|
|
995
|
+
return void 0;
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
return new URL(baseUrl).hostname;
|
|
999
|
+
} catch {
|
|
1000
|
+
return void 0;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
755
1003
|
export {
|
|
756
1004
|
DEFAULT_IGNORE_PATHS,
|
|
757
1005
|
DEFAULT_WATCH_PATHS,
|
|
@@ -760,6 +1008,10 @@ export {
|
|
|
760
1008
|
createEngine,
|
|
761
1009
|
createRipplo,
|
|
762
1010
|
lint,
|
|
1011
|
+
notImplemented,
|
|
1012
|
+
observer,
|
|
1013
|
+
precondition,
|
|
763
1014
|
serializeCookie,
|
|
1015
|
+
test,
|
|
764
1016
|
verifyWebhookSignature
|
|
765
1017
|
};
|