@ripplo/testing 0.0.11 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -27
- package/dist/actions.d.ts +11 -4
- package/dist/actions.js +11 -6
- package/dist/assert.d.ts +12 -1
- package/dist/assert.js +19 -0
- package/dist/builder-c7tXey03.d.ts +80 -0
- package/dist/chunk-3IL457A7.js +60 -0
- package/dist/{chunk-7ETQVVAA.js → chunk-CD3M7H5A.js} +127 -46
- package/dist/{chunk-LEIKZ6BE.js → chunk-KNF4K4JH.js} +12 -4
- package/dist/compiler.d.ts +5 -3
- package/dist/compiler.js +1 -1
- package/dist/control.d.ts +1 -1
- package/dist/express.d.ts +3 -2
- package/dist/express.js +19 -3
- package/dist/fastify.d.ts +3 -2
- package/dist/fastify.js +17 -3
- package/dist/index.d.ts +13 -5
- package/dist/index.js +158 -44
- package/dist/lockfile.d.ts +68 -76
- package/dist/lockfile.js +33 -13
- package/dist/nextjs.d.ts +3 -2
- package/dist/nextjs.js +29 -5
- package/dist/{step-DLfkKI3V.d.ts → step-De52hTLd.d.ts} +1 -1
- package/dist/types-oYS_Yv4G.d.ts +115 -0
- package/package.json +3 -3
- package/dist/builder-1kySbit_.d.ts +0 -137
package/dist/index.js
CHANGED
|
@@ -1,62 +1,97 @@
|
|
|
1
1
|
import {
|
|
2
2
|
compile
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KNF4K4JH.js";
|
|
4
4
|
import "./chunk-MGATMMCZ.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
DEFAULT_WATCH_PATHS,
|
|
6
|
+
buildObserver,
|
|
8
7
|
buildSetCookieHeader,
|
|
9
8
|
createEngine,
|
|
10
|
-
dslConfigSchema,
|
|
11
|
-
readPreconditionName,
|
|
12
9
|
serializeCookie,
|
|
13
10
|
verifyWebhookSignature
|
|
14
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-CD3M7H5A.js";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_IGNORE_PATHS,
|
|
14
|
+
DEFAULT_WATCH_PATHS,
|
|
15
|
+
dslConfigSchema,
|
|
16
|
+
readObserverName,
|
|
17
|
+
readPreconditionName
|
|
18
|
+
} from "./chunk-3IL457A7.js";
|
|
15
19
|
|
|
16
20
|
// src/builder.ts
|
|
17
21
|
function createRipplo(rawConfig) {
|
|
18
22
|
const config = dslConfigSchema.parse(rawConfig);
|
|
19
23
|
const preconditions = [];
|
|
24
|
+
const observers = [];
|
|
20
25
|
const tests = [];
|
|
21
26
|
const preconditionNames = /* @__PURE__ */ new Set();
|
|
27
|
+
const observerNames = /* @__PURE__ */ new Set();
|
|
22
28
|
const testNames = /* @__PURE__ */ new Set();
|
|
29
|
+
function implementPrecondition(handle, impl) {
|
|
30
|
+
const preconditionName = readPreconditionName(handle);
|
|
31
|
+
const idx = preconditions.findIndex((p) => p.name === preconditionName);
|
|
32
|
+
if (idx === -1) {
|
|
33
|
+
throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
|
|
34
|
+
}
|
|
35
|
+
const existing = preconditions[idx];
|
|
36
|
+
if (existing == null) {
|
|
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
|
+
}
|
|
23
75
|
return {
|
|
76
|
+
implementObserver,
|
|
77
|
+
implementPrecondition,
|
|
24
78
|
getConfig: () => config,
|
|
79
|
+
getObservers: () => observers,
|
|
25
80
|
getPreconditions: () => preconditions,
|
|
26
81
|
getTests: () => tests,
|
|
27
82
|
getUnimplemented() {
|
|
28
83
|
return {
|
|
84
|
+
observers: observers.filter((o) => !o.implemented).map((o) => o.name),
|
|
29
85
|
preconditions: preconditions.filter((p) => !p.implemented).map((p) => p.name),
|
|
30
86
|
tests: tests.filter((t) => !t.implemented).map((t) => t.name)
|
|
31
87
|
};
|
|
32
88
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (idx === -1) {
|
|
37
|
-
throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
|
|
89
|
+
observer(name) {
|
|
90
|
+
if (observerNames.has(name)) {
|
|
91
|
+
observers.splice(0, observers.length, ...observers.filter((o) => o.name !== name));
|
|
38
92
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
throw new Error(`Cannot implement unknown precondition: "${preconditionName}"`);
|
|
42
|
-
}
|
|
43
|
-
const mapping = existing.depMapping;
|
|
44
|
-
preconditions[idx] = {
|
|
45
|
-
...existing,
|
|
46
|
-
implemented: true,
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- TData narrows Record<string, string>, safe at engine boundary
|
|
48
|
-
teardown: impl.teardown,
|
|
49
|
-
setup: async (ctx, allDeps) => {
|
|
50
|
-
const resolved = {};
|
|
51
|
-
mapping.forEach(([key, depName]) => {
|
|
52
|
-
const data = allDeps[depName];
|
|
53
|
-
if (data != null) {
|
|
54
|
-
resolved[key] = data;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
return impl.setup(ctx, resolved);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
93
|
+
observerNames.add(name);
|
|
94
|
+
return buildObserver({ name, observers });
|
|
60
95
|
},
|
|
61
96
|
precondition(name) {
|
|
62
97
|
if (preconditionNames.has(name)) {
|
|
@@ -69,13 +104,13 @@ function createRipplo(rawConfig) {
|
|
|
69
104
|
preconditionNames.add(name);
|
|
70
105
|
return buildPrecondition(name, preconditions);
|
|
71
106
|
},
|
|
72
|
-
test(id) {
|
|
107
|
+
test(id, options) {
|
|
73
108
|
validateTestId(id);
|
|
74
109
|
if (testNames.has(id)) {
|
|
75
110
|
tests.splice(0, tests.length, ...tests.filter((t) => t.id !== id));
|
|
76
111
|
}
|
|
77
112
|
testNames.add(id);
|
|
78
|
-
return buildTestName(id, tests);
|
|
113
|
+
return buildTestName(id, tests, options?.uiOnly);
|
|
79
114
|
}
|
|
80
115
|
};
|
|
81
116
|
}
|
|
@@ -187,14 +222,19 @@ function validateTestId(id) {
|
|
|
187
222
|
);
|
|
188
223
|
}
|
|
189
224
|
}
|
|
190
|
-
function buildTestName(id, tests) {
|
|
225
|
+
function buildTestName(id, tests, uiOnly) {
|
|
191
226
|
return {
|
|
192
227
|
name(displayName) {
|
|
193
|
-
return buildTestRequires({ id, name: displayName, tests });
|
|
228
|
+
return buildTestRequires({ id, name: displayName, tests, uiOnly });
|
|
194
229
|
}
|
|
195
230
|
};
|
|
196
231
|
}
|
|
197
|
-
function buildTestRequires({
|
|
232
|
+
function buildTestRequires({
|
|
233
|
+
id,
|
|
234
|
+
name,
|
|
235
|
+
tests,
|
|
236
|
+
uiOnly
|
|
237
|
+
}) {
|
|
198
238
|
let description = "";
|
|
199
239
|
const self = {
|
|
200
240
|
requires: castBuilder(requiresImpl),
|
|
@@ -210,7 +250,7 @@ function buildTestRequires({ id, name, tests }) {
|
|
|
210
250
|
Object.entries(reqs).forEach(([key, precondition]) => {
|
|
211
251
|
requiresKeys[key] = readPreconditionName(precondition);
|
|
212
252
|
});
|
|
213
|
-
return buildOutcome({ description, id, name, reqNames, requiresKeys, tests });
|
|
253
|
+
return buildOutcome({ description, id, name, reqNames, requiresKeys, tests, uiOnly });
|
|
214
254
|
}
|
|
215
255
|
}
|
|
216
256
|
function buildOutcome({
|
|
@@ -219,7 +259,8 @@ function buildOutcome({
|
|
|
219
259
|
name,
|
|
220
260
|
reqNames,
|
|
221
261
|
requiresKeys,
|
|
222
|
-
tests
|
|
262
|
+
tests,
|
|
263
|
+
uiOnly
|
|
223
264
|
}) {
|
|
224
265
|
let description = initialDesc;
|
|
225
266
|
return {
|
|
@@ -235,7 +276,8 @@ function buildOutcome({
|
|
|
235
276
|
name,
|
|
236
277
|
reqNames,
|
|
237
278
|
requiresKeys,
|
|
238
|
-
tests
|
|
279
|
+
tests,
|
|
280
|
+
uiOnly
|
|
239
281
|
});
|
|
240
282
|
}
|
|
241
283
|
};
|
|
@@ -247,7 +289,8 @@ function buildStartsAt({
|
|
|
247
289
|
name,
|
|
248
290
|
reqNames,
|
|
249
291
|
requiresKeys,
|
|
250
|
-
tests
|
|
292
|
+
tests,
|
|
293
|
+
uiOnly
|
|
251
294
|
}) {
|
|
252
295
|
return {
|
|
253
296
|
startsAt: castBuilder(startsAtImpl),
|
|
@@ -261,7 +304,8 @@ function buildStartsAt({
|
|
|
261
304
|
requires: [...reqNames],
|
|
262
305
|
requiresKeys,
|
|
263
306
|
startsAtFn: void 0,
|
|
264
|
-
stepsFn: void 0
|
|
307
|
+
stepsFn: void 0,
|
|
308
|
+
uiOnly
|
|
265
309
|
});
|
|
266
310
|
}
|
|
267
311
|
};
|
|
@@ -279,7 +323,8 @@ function buildStartsAt({
|
|
|
279
323
|
requires: [...reqNames],
|
|
280
324
|
requiresKeys,
|
|
281
325
|
startsAtFn: fn,
|
|
282
|
-
stepsFn
|
|
326
|
+
stepsFn,
|
|
327
|
+
uiOnly
|
|
283
328
|
});
|
|
284
329
|
}
|
|
285
330
|
}
|
|
@@ -624,6 +669,73 @@ function expectedOutcomeKeywordCoverage(nodes, test, report) {
|
|
|
624
669
|
});
|
|
625
670
|
}
|
|
626
671
|
}
|
|
672
|
+
var BACKEND_MUTATION_KEYWORDS = /save|submit|create|delete|remove|send|invite|update|confirm|publish|apply|pay|subscribe|upgrade|cancel|archive|rename/i;
|
|
673
|
+
function isLikelyBackendMutation(node) {
|
|
674
|
+
if (node.type === "upload") {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
if (node.type === "handleDialog" && node.action === "accept") {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
if (node.type !== "click") {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
const loc = node.locator;
|
|
684
|
+
const name = loc.by === "role" ? loc.name ?? "" : loc.value;
|
|
685
|
+
return BACKEND_MUTATION_KEYWORDS.test(name);
|
|
686
|
+
}
|
|
687
|
+
function mutationWithoutObserverCoverage(nodes, test, report) {
|
|
688
|
+
if (!test.implemented || test.spec.uiOnly === true) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
nodes.forEach((node, index) => {
|
|
692
|
+
if (!isLikelyBackendMutation(node)) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if ("uiOnly" in node && node.uiOnly === true) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const rest = nodes.slice(index + 1);
|
|
699
|
+
const nextMutationIdx = rest.findIndex((n) => isLikelyBackendMutation(n));
|
|
700
|
+
const windowEnd = nextMutationIdx === -1 ? rest.length : nextMutationIdx;
|
|
701
|
+
const window = rest.slice(0, windowEnd);
|
|
702
|
+
const hasObserver = window.some((n) => n.type === "assertObserver");
|
|
703
|
+
if (hasObserver) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
report({
|
|
707
|
+
message: `"${node.label ?? node.id}" looks like it mutates backend state but no assert.backend(...) observer verifies it \u2014 the test can pass while the server is broken. You need to add an observer that checks the persisted result (see .ripplo/observers/ for examples). Only if this action truly does NOT change any server state (pure client-side interaction, presentation toggle, etc.) should you fall back to marking the step { uiOnly: true } to silence this rule.`,
|
|
708
|
+
rule: "mutation-without-observer-coverage",
|
|
709
|
+
step: node.label ?? node.id
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function observerParamsReferenceVariables(nodes, test, report) {
|
|
714
|
+
const variableKeys = Object.keys(test.spec.variables ?? {});
|
|
715
|
+
if (variableKeys.length === 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
nodes.forEach((node) => {
|
|
719
|
+
if (node.type !== "assertObserver") {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const paramEntries = Object.entries(node.params);
|
|
723
|
+
if (paramEntries.length === 0) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const anyReferencesVariable = paramEntries.some(([, ref]) => {
|
|
727
|
+
return isStaticStringValue(ref) && isTemplateVar(ref.value);
|
|
728
|
+
});
|
|
729
|
+
if (anyReferencesVariable) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
report({
|
|
733
|
+
message: `assert.backend "${node.label ?? node.id}" passes only hardcoded params while the test defines precondition variables \u2014 observer assertions should reference {{namespace.key}} so they match the actual precondition data`,
|
|
734
|
+
rule: "observer-params-reference-variables",
|
|
735
|
+
step: node.label ?? node.id
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
627
739
|
var RULES = [
|
|
628
740
|
exactTextMatch,
|
|
629
741
|
noHardcodedData,
|
|
@@ -636,7 +748,9 @@ var RULES = [
|
|
|
636
748
|
noAssertions,
|
|
637
749
|
lowAssertionRatio,
|
|
638
750
|
tautologicalPostClickAssert,
|
|
639
|
-
expectedOutcomeKeywordCoverage
|
|
751
|
+
expectedOutcomeKeywordCoverage,
|
|
752
|
+
mutationWithoutObserverCoverage,
|
|
753
|
+
observerParamsReferenceVariables
|
|
640
754
|
];
|
|
641
755
|
export {
|
|
642
756
|
DEFAULT_IGNORE_PATHS,
|