@reventlessdev/reventless-gwt 1.0.0-alpha.75 → 1.0.0-alpha.77
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/CHANGELOG.md +16 -0
- package/package.json +8 -8
- package/src/Behavior_GWT.res +125 -159
- package/src/Behavior_GWT.res.mjs +156 -59
- package/src/Bind.res.mjs +2 -2
- package/src/BuildClassifier.res +15 -5
- package/src/BuildClassifier.res.mjs +3 -3
- package/src/Cancellation.res +6 -0
- package/src/Cancellation.res.mjs +3 -2
- package/src/ChildProcess.res +19 -1
- package/src/ChildProcess.res.mjs +16 -1
- package/src/Cli.res +260 -26
- package/src/Cli.res.mjs +227 -29
- package/src/Collector.res +13 -2
- package/src/Collector.res.mjs +8 -5
- package/src/ComponentMeta.res +16 -43
- package/src/ComponentMeta.res.mjs +12 -41
- package/src/Discovery.res +19 -19
- package/src/Discovery.res.mjs +5 -9
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/Filter.res +11 -2
- package/src/Filter.res.mjs +10 -3
- package/src/FormatterHuman.res +14 -43
- package/src/FormatterHuman.res.mjs +21 -28
- package/src/FormatterJson.res +12 -5
- package/src/FormatterJson.res.mjs +8 -6
- package/src/FormatterTap.res +13 -52
- package/src/FormatterTap.res.mjs +25 -43
- package/src/FormatterVsCode.res +4 -19
- package/src/FormatterVsCode.res.mjs +4 -23
- package/src/JestBind.res +1 -1
- package/src/JestBind.res.mjs +2 -2
- package/src/LocalHost.res +11 -27
- package/src/LocalHost.res.mjs +6 -34
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -0
- package/src/Outcome.res +0 -64
- package/src/Outcome.res.mjs +0 -47
- package/src/PlatformRunner.res +27 -6
- package/src/PlatformRunner.res.mjs +27 -6
- package/src/PlatformScan.res +14 -10
- package/src/PlatformScan.res.mjs +14 -13
- package/src/ProcessManager.res +51 -7
- package/src/ProcessManager.res.mjs +43 -4
- package/src/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -0
- package/src/Watch.res +46 -30
- package/src/Watch.res.mjs +34 -32
- package/src/WatcherProbe.res +11 -3
- package/src/WatcherProbe.res.mjs +15 -2
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/BuildClassifierTest.res +39 -0
- package/tests/BuildClassifierTest.res.mjs +44 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +15 -0
- package/tests/ComponentMetaTest.res.mjs +14 -0
- package/tests/DomainGraphTest.res +2 -2
- package/tests/FlowAggregateGwtTest.res +2 -0
- package/tests/FlowAggregateGwtTest.res.mjs +4 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -0
- package/tests/MappingGwtTest.res +2 -0
- package/tests/MappingGwtTest.res.mjs +4 -0
- package/tests/WatchDebounceTest.res +35 -7
- package/tests/WatchDebounceTest.res.mjs +37 -6
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Regression tests for the CLI runner's per-test deadline (A2) and the
|
|
2
|
+
// Collector's skip-depth reset (A4). Before A2 a hung test body wedged the whole
|
|
3
|
+
// run — `runEntry` awaited it with no deadline. Before A4 a throwing `xdescribe`
|
|
4
|
+
// left `skipDepth` > 0, silently skipping every subsequently loaded file; the
|
|
5
|
+
// reset lives in `Collector.activate`.
|
|
6
|
+
|
|
7
|
+
open JestGlobals
|
|
8
|
+
|
|
9
|
+
@val external setTimeout: (unit => unit, int) => unit = "setTimeout"
|
|
10
|
+
|
|
11
|
+
let hangingEntry = (~timeout): Collector.entry => {
|
|
12
|
+
id: "hangs",
|
|
13
|
+
name: "hangs forever",
|
|
14
|
+
describePath: [],
|
|
15
|
+
slice: None,
|
|
16
|
+
// A body that never resolves — the pathological hung test.
|
|
17
|
+
body: () => Promise.make((_resolve, _reject) => ()),
|
|
18
|
+
status: Collector.Runnable,
|
|
19
|
+
location: None,
|
|
20
|
+
timeout,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Cli.runEntry deadline", () => {
|
|
24
|
+
testPromise("a hung body is reported as a timeout, not a wedge", async () => {
|
|
25
|
+
let r = await Cli.runEntry(hangingEntry(~timeout=Some(50)))
|
|
26
|
+
expect(r.status)->toEqual(RunnerTypes.Fail)
|
|
27
|
+
switch r.mismatch {
|
|
28
|
+
| Some(Outcome.Throw({error})) => expect(error->String.includes("timed out"))->toEqual(true)
|
|
29
|
+
| _ => JsError.throwWithMessage("expected a timeout Throw mismatch")
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
testPromise("a body that resolves within the deadline passes", async () => {
|
|
34
|
+
let entry: Collector.entry = {
|
|
35
|
+
id: "ok",
|
|
36
|
+
name: "resolves fast",
|
|
37
|
+
describePath: [],
|
|
38
|
+
slice: None,
|
|
39
|
+
body: () => Promise.resolve(Outcome.pass),
|
|
40
|
+
status: Collector.Runnable,
|
|
41
|
+
location: None,
|
|
42
|
+
timeout: Some(1000),
|
|
43
|
+
}
|
|
44
|
+
let r = await Cli.runEntry(entry)
|
|
45
|
+
expect(r.status)->toEqual(RunnerTypes.Pass)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("Collector.activate", () => {
|
|
50
|
+
testSync("resets a leaked skipDepth to zero", () => {
|
|
51
|
+
// Simulate the leak a throwing xdescribe body would leave behind.
|
|
52
|
+
Collector.skipDepth := 3
|
|
53
|
+
Collector.activate()
|
|
54
|
+
expect(Collector.skipDepth.contents)->toEqual(0)
|
|
55
|
+
Collector.deactivate()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
|
+
import * as Cli$ReventlessGwt from "../src/Cli.res.mjs";
|
|
5
|
+
import * as Outcome$ReventlessGwt from "../src/Outcome.res.mjs";
|
|
6
|
+
import * as Collector$ReventlessGwt from "../src/Collector.res.mjs";
|
|
7
|
+
|
|
8
|
+
function hangingEntry(timeout) {
|
|
9
|
+
return {
|
|
10
|
+
id: "hangs",
|
|
11
|
+
name: "hangs forever",
|
|
12
|
+
describePath: [],
|
|
13
|
+
slice: undefined,
|
|
14
|
+
body: () => new Promise((_resolve, _reject) => {}),
|
|
15
|
+
status: "Runnable",
|
|
16
|
+
location: undefined,
|
|
17
|
+
timeout: timeout
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
globalThis.describe("Cli.runEntry deadline", () => {
|
|
22
|
+
globalThis.test("a hung body is reported as a timeout, not a wedge", async () => {
|
|
23
|
+
let r = await Cli$ReventlessGwt.runEntry(hangingEntry(50));
|
|
24
|
+
globalThis.expect(r.status).toEqual("Fail");
|
|
25
|
+
let match = r.mismatch;
|
|
26
|
+
if (match === undefined) {
|
|
27
|
+
return Stdlib_JsError.throwWithMessage("expected a timeout Throw mismatch");
|
|
28
|
+
}
|
|
29
|
+
if (match.TAG !== "Throw") {
|
|
30
|
+
return Stdlib_JsError.throwWithMessage("expected a timeout Throw mismatch");
|
|
31
|
+
}
|
|
32
|
+
globalThis.expect(match.error.includes("timed out")).toEqual(true);
|
|
33
|
+
});
|
|
34
|
+
globalThis.test("a body that resolves within the deadline passes", async () => {
|
|
35
|
+
let entry_describePath = [];
|
|
36
|
+
let entry_body = () => Promise.resolve(Outcome$ReventlessGwt.pass);
|
|
37
|
+
let entry_timeout = 1000;
|
|
38
|
+
let entry = {
|
|
39
|
+
id: "ok",
|
|
40
|
+
name: "resolves fast",
|
|
41
|
+
describePath: entry_describePath,
|
|
42
|
+
slice: undefined,
|
|
43
|
+
body: entry_body,
|
|
44
|
+
status: "Runnable",
|
|
45
|
+
location: undefined,
|
|
46
|
+
timeout: entry_timeout
|
|
47
|
+
};
|
|
48
|
+
let r = await Cli$ReventlessGwt.runEntry(entry);
|
|
49
|
+
globalThis.expect(r.status).toEqual("Pass");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
globalThis.describe("Collector.activate", () => {
|
|
54
|
+
globalThis.test("resets a leaked skipDepth to zero", () => {
|
|
55
|
+
Collector$ReventlessGwt.skipDepth.contents = 3;
|
|
56
|
+
Collector$ReventlessGwt.activate();
|
|
57
|
+
globalThis.expect(Collector$ReventlessGwt.skipDepth.contents).toEqual(0);
|
|
58
|
+
Collector$ReventlessGwt.deactivate();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
hangingEntry,
|
|
64
|
+
}
|
|
65
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Unit tests for the watch re-run scope algebra (B1): a plain edit narrows the
|
|
2
|
+
// re-run to its owning test package, structural/add/full triggers widen to
|
|
3
|
+
// RunAll, and scopes coalesce (RunAll absorbing) while a pass is in flight.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
describe("Cli.mergeScope", () => {
|
|
8
|
+
testSync("RunAll absorbs on the left", () =>
|
|
9
|
+
expect(Cli.mergeScope(RunAll, RunPackages(["/p/a"])))->toEqual(Cli.RunAll)
|
|
10
|
+
)
|
|
11
|
+
testSync("RunAll absorbs on the right", () =>
|
|
12
|
+
expect(Cli.mergeScope(RunPackages(["/p/a"]), RunAll))->toEqual(Cli.RunAll)
|
|
13
|
+
)
|
|
14
|
+
testSync("two RunPackages union their dirs, deduplicated", () =>
|
|
15
|
+
expect(Cli.mergeScope(RunPackages(["/p/a", "/p/b"]), RunPackages(["/p/b", "/p/c"])))->toEqual(
|
|
16
|
+
Cli.RunPackages(["/p/a", "/p/b", "/p/c"]),
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
testSync("RunAll merged with RunAll stays RunAll", () =>
|
|
20
|
+
expect(Cli.mergeScope(RunAll, RunAll))->toEqual(Cli.RunAll)
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe("Cli.pathUnderDir", () => {
|
|
25
|
+
testSync("a file inside the dir is under it", () =>
|
|
26
|
+
expect(Cli.pathUnderDir("/p/pkgA/tests/FooGwt.res.mjs", "/p/pkgA"))->toEqual(true)
|
|
27
|
+
)
|
|
28
|
+
testSync("a sibling with a shared prefix is NOT under it", () =>
|
|
29
|
+
// /p/pkgABC must not be treated as inside /p/pkgA — the trailing separator
|
|
30
|
+
// guards against the bare-prefix false positive.
|
|
31
|
+
expect(Cli.pathUnderDir("/p/pkgABC/tests/FooGwt.res.mjs", "/p/pkgA"))->toEqual(false)
|
|
32
|
+
)
|
|
33
|
+
testSync("an unrelated path is not under it", () =>
|
|
34
|
+
expect(Cli.pathUnderDir("/p/pkgB/tests/BarGwt.res.mjs", "/p/pkgA"))->toEqual(false)
|
|
35
|
+
)
|
|
36
|
+
testSync("a trailing-slash dir still matches", () =>
|
|
37
|
+
expect(Cli.pathUnderDir("/p/pkgA/tests/FooGwt.res.mjs", "/p/pkgA/"))->toEqual(true)
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe("Cli.scopeSubset", () => {
|
|
42
|
+
let all = [
|
|
43
|
+
"/p/pkgA/tests/FooGwt.res.mjs",
|
|
44
|
+
"/p/pkgA/tests/BarGwt.res.mjs",
|
|
45
|
+
"/p/pkgB/tests/BazGwt.res.mjs",
|
|
46
|
+
]
|
|
47
|
+
testSync("RunAll returns every path", () =>
|
|
48
|
+
expect(Cli.scopeSubset(RunAll, all))->toEqual(all)
|
|
49
|
+
)
|
|
50
|
+
testSync("RunPackages keeps only files under the given dirs", () =>
|
|
51
|
+
expect(Cli.scopeSubset(RunPackages(["/p/pkgA"]), all))->toEqual([
|
|
52
|
+
"/p/pkgA/tests/FooGwt.res.mjs",
|
|
53
|
+
"/p/pkgA/tests/BarGwt.res.mjs",
|
|
54
|
+
])
|
|
55
|
+
)
|
|
56
|
+
testSync("RunPackages over multiple dirs unions their files", () =>
|
|
57
|
+
expect(Cli.scopeSubset(RunPackages(["/p/pkgA", "/p/pkgB"]), all))->toEqual(all)
|
|
58
|
+
)
|
|
59
|
+
testSync("RunPackages with no matching dir yields no files", () =>
|
|
60
|
+
expect(Cli.scopeSubset(RunPackages(["/p/pkgZ"]), all))->toEqual([])
|
|
61
|
+
)
|
|
62
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Cli$ReventlessGwt from "../src/Cli.res.mjs";
|
|
4
|
+
|
|
5
|
+
globalThis.describe("Cli.mergeScope", () => {
|
|
6
|
+
globalThis.test("RunAll absorbs on the left", () => {
|
|
7
|
+
globalThis.expect(Cli$ReventlessGwt.mergeScope("RunAll", {
|
|
8
|
+
TAG: "RunPackages",
|
|
9
|
+
_0: ["/p/a"]
|
|
10
|
+
})).toEqual("RunAll");
|
|
11
|
+
});
|
|
12
|
+
globalThis.test("RunAll absorbs on the right", () => {
|
|
13
|
+
globalThis.expect(Cli$ReventlessGwt.mergeScope({
|
|
14
|
+
TAG: "RunPackages",
|
|
15
|
+
_0: ["/p/a"]
|
|
16
|
+
}, "RunAll")).toEqual("RunAll");
|
|
17
|
+
});
|
|
18
|
+
globalThis.test("two RunPackages union their dirs, deduplicated", () => {
|
|
19
|
+
globalThis.expect(Cli$ReventlessGwt.mergeScope({
|
|
20
|
+
TAG: "RunPackages",
|
|
21
|
+
_0: [
|
|
22
|
+
"/p/a",
|
|
23
|
+
"/p/b"
|
|
24
|
+
]
|
|
25
|
+
}, {
|
|
26
|
+
TAG: "RunPackages",
|
|
27
|
+
_0: [
|
|
28
|
+
"/p/b",
|
|
29
|
+
"/p/c"
|
|
30
|
+
]
|
|
31
|
+
})).toEqual({
|
|
32
|
+
TAG: "RunPackages",
|
|
33
|
+
_0: [
|
|
34
|
+
"/p/a",
|
|
35
|
+
"/p/b",
|
|
36
|
+
"/p/c"
|
|
37
|
+
]
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("RunAll merged with RunAll stays RunAll", () => {
|
|
41
|
+
globalThis.expect(Cli$ReventlessGwt.mergeScope("RunAll", "RunAll")).toEqual("RunAll");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
globalThis.describe("Cli.pathUnderDir", () => {
|
|
46
|
+
globalThis.test("a file inside the dir is under it", () => {
|
|
47
|
+
globalThis.expect(Cli$ReventlessGwt.pathUnderDir("/p/pkgA/tests/FooGwt.res.mjs", "/p/pkgA")).toEqual(true);
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("a sibling with a shared prefix is NOT under it", () => {
|
|
50
|
+
globalThis.expect(Cli$ReventlessGwt.pathUnderDir("/p/pkgABC/tests/FooGwt.res.mjs", "/p/pkgA")).toEqual(false);
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("an unrelated path is not under it", () => {
|
|
53
|
+
globalThis.expect(Cli$ReventlessGwt.pathUnderDir("/p/pkgB/tests/BarGwt.res.mjs", "/p/pkgA")).toEqual(false);
|
|
54
|
+
});
|
|
55
|
+
globalThis.test("a trailing-slash dir still matches", () => {
|
|
56
|
+
globalThis.expect(Cli$ReventlessGwt.pathUnderDir("/p/pkgA/tests/FooGwt.res.mjs", "/p/pkgA/")).toEqual(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
globalThis.describe("Cli.scopeSubset", () => {
|
|
61
|
+
let all = [
|
|
62
|
+
"/p/pkgA/tests/FooGwt.res.mjs",
|
|
63
|
+
"/p/pkgA/tests/BarGwt.res.mjs",
|
|
64
|
+
"/p/pkgB/tests/BazGwt.res.mjs"
|
|
65
|
+
];
|
|
66
|
+
globalThis.test("RunAll returns every path", () => {
|
|
67
|
+
globalThis.expect(Cli$ReventlessGwt.scopeSubset("RunAll", all)).toEqual(all);
|
|
68
|
+
});
|
|
69
|
+
globalThis.test("RunPackages keeps only files under the given dirs", () => {
|
|
70
|
+
globalThis.expect(Cli$ReventlessGwt.scopeSubset({
|
|
71
|
+
TAG: "RunPackages",
|
|
72
|
+
_0: ["/p/pkgA"]
|
|
73
|
+
}, all)).toEqual([
|
|
74
|
+
"/p/pkgA/tests/FooGwt.res.mjs",
|
|
75
|
+
"/p/pkgA/tests/BarGwt.res.mjs"
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
globalThis.test("RunPackages over multiple dirs unions their files", () => {
|
|
79
|
+
globalThis.expect(Cli$ReventlessGwt.scopeSubset({
|
|
80
|
+
TAG: "RunPackages",
|
|
81
|
+
_0: [
|
|
82
|
+
"/p/pkgA",
|
|
83
|
+
"/p/pkgB"
|
|
84
|
+
]
|
|
85
|
+
}, all)).toEqual(all);
|
|
86
|
+
});
|
|
87
|
+
globalThis.test("RunPackages with no matching dir yields no files", () => {
|
|
88
|
+
globalThis.expect(Cli$ReventlessGwt.scopeSubset({
|
|
89
|
+
TAG: "RunPackages",
|
|
90
|
+
_0: ["/p/pkgZ"]
|
|
91
|
+
}, all)).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/* Not a pure module */
|
|
@@ -28,6 +28,21 @@ describe("ComponentMeta.componentOfTestFile", () => {
|
|
|
28
28
|
let c = ComponentMeta.componentOfTestFile("/repo/x/tests/Helpers/Thing_GWT.res.mjs")
|
|
29
29
|
expect(c)->toEqual(None)
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
testPromise("recognises a plural folder spelling with the canonical kind (C2)", async () => {
|
|
33
|
+
// The generator accepts `Aggregates/`; previously the gwt discovery only knew
|
|
34
|
+
// the singular `Aggregate/` and silently returned None. Now it derives from
|
|
35
|
+
// the shared vocabulary and reports the canonical singular kind.
|
|
36
|
+
let c = ComponentMeta.componentOfTestFile("/repo/x/tests/Order/Aggregates/Order_GWT.res.mjs")
|
|
37
|
+
expect(c)->toEqual(Some({ComponentMeta.kind: "Aggregate", name: "Order"}))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
testPromise("recognises a short slice folder spelling (StateChange) too (C2)", async () => {
|
|
41
|
+
let c = ComponentMeta.componentOfTestFile(
|
|
42
|
+
"/repo/catalog/tests/Product/StateChange/AddProduct_GWT.res.mjs",
|
|
43
|
+
)
|
|
44
|
+
expect(c)->toEqual(Some({ComponentMeta.kind: "StateChangeSlice", name: "AddProduct"}))
|
|
45
|
+
})
|
|
31
46
|
})
|
|
32
47
|
|
|
33
48
|
describe("ComponentMeta.componentOfSrcFile", () => {
|
|
@@ -28,6 +28,20 @@ globalThis.describe("ComponentMeta.componentOfTestFile", () => {
|
|
|
28
28
|
let c = ComponentMeta$ReventlessGwt.componentOfTestFile("/repo/x/tests/Helpers/Thing_GWT.res.mjs");
|
|
29
29
|
globalThis.expect(c).toEqual(undefined);
|
|
30
30
|
});
|
|
31
|
+
globalThis.test("recognises a plural folder spelling with the canonical kind (C2)", async () => {
|
|
32
|
+
let c = ComponentMeta$ReventlessGwt.componentOfTestFile("/repo/x/tests/Order/Aggregates/Order_GWT.res.mjs");
|
|
33
|
+
globalThis.expect(c).toEqual({
|
|
34
|
+
kind: "Aggregate",
|
|
35
|
+
name: "Order"
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
globalThis.test("recognises a short slice folder spelling (StateChange) too (C2)", async () => {
|
|
39
|
+
let c = ComponentMeta$ReventlessGwt.componentOfTestFile("/repo/catalog/tests/Product/StateChange/AddProduct_GWT.res.mjs");
|
|
40
|
+
globalThis.expect(c).toEqual({
|
|
41
|
+
kind: "StateChangeSlice",
|
|
42
|
+
name: "AddProduct"
|
|
43
|
+
});
|
|
44
|
+
});
|
|
31
45
|
});
|
|
32
46
|
|
|
33
47
|
globalThis.describe("ComponentMeta.componentOfSrcFile", () => {
|
|
@@ -84,9 +84,9 @@ let structure = (
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
let hasEdge = (g: DomainGraph.graph, from, to, kind) =>
|
|
87
|
-
g.edges->Array.some(e => e.from == from && e.
|
|
87
|
+
g.edges->Array.some(e => e.from == from && e.to_ == to && e.kind == kind)
|
|
88
88
|
let edgeLabel = (g: DomainGraph.graph, from, to, kind) =>
|
|
89
|
-
g.edges->Array.find(e => e.from == from && e.
|
|
89
|
+
g.edges->Array.find(e => e.from == from && e.to_ == to && e.kind == kind)->Option.flatMap(e => e.label)
|
|
90
90
|
let nodeKind = (g: DomainGraph.graph, id) =>
|
|
91
91
|
g.nodes->Array.find(n => n.id == id)->Option.map(n => n.kind)
|
|
92
92
|
|
|
@@ -34,6 +34,7 @@ module CatalogProductBehavior = {
|
|
|
34
34
|
type state = NotSynced | Synced
|
|
35
35
|
|
|
36
36
|
let initialState = NotSynced
|
|
37
|
+
let snapshot = None
|
|
37
38
|
|
|
38
39
|
let evolve = (_state, event: CatalogProductAggregate.event) =>
|
|
39
40
|
switch event {
|
|
@@ -77,6 +78,7 @@ module OrderBehavior = {
|
|
|
77
78
|
type state = {placed: bool, shipped: bool}
|
|
78
79
|
|
|
79
80
|
let initialState = {placed: false, shipped: false}
|
|
81
|
+
let snapshot = None
|
|
80
82
|
|
|
81
83
|
let evolve = (state, event: OrderAggregate.event) =>
|
|
82
84
|
switch event {
|
|
@@ -58,6 +58,7 @@ let moduleUrl = "test://CatalogProductBehavior";
|
|
|
58
58
|
let CatalogProductBehavior = {
|
|
59
59
|
Spec: undefined,
|
|
60
60
|
initialState: "NotSynced",
|
|
61
|
+
snapshot: undefined,
|
|
61
62
|
evolve: evolve,
|
|
62
63
|
decide: decide,
|
|
63
64
|
moduleUrl: moduleUrl
|
|
@@ -159,6 +160,7 @@ let moduleUrl$1 = "test://OrderBehavior";
|
|
|
159
160
|
let OrderBehavior = {
|
|
160
161
|
Spec: undefined,
|
|
161
162
|
initialState: initialState,
|
|
163
|
+
snapshot: undefined,
|
|
162
164
|
evolve: evolve$1,
|
|
163
165
|
decide: decide$1,
|
|
164
166
|
moduleUrl: moduleUrl$1
|
|
@@ -169,6 +171,7 @@ let Sync = Flow_GWT$ReventlessGwt.AggregateCommandStep(CatalogProductAggregate)(
|
|
|
169
171
|
initialState: "NotSynced",
|
|
170
172
|
evolve: evolve,
|
|
171
173
|
decide: decide,
|
|
174
|
+
snapshot: undefined,
|
|
172
175
|
moduleUrl: moduleUrl
|
|
173
176
|
});
|
|
174
177
|
|
|
@@ -177,6 +180,7 @@ let Place = Flow_GWT$ReventlessGwt.AggregateCommandStep(OrderAggregate)({
|
|
|
177
180
|
initialState: initialState,
|
|
178
181
|
evolve: evolve$1,
|
|
179
182
|
decide: decide$1,
|
|
183
|
+
snapshot: undefined,
|
|
180
184
|
moduleUrl: moduleUrl$1
|
|
181
185
|
});
|
|
182
186
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Golden-output tests pinning the per-mismatch rendering of the three
|
|
2
|
+
// string-emitting formatters:
|
|
3
|
+
// - `Outcome.format` (used by the JUnit `<failure>` body)
|
|
4
|
+
// - `FormatterHuman.renderMismatch` (terminal output)
|
|
5
|
+
// - `FormatterTap.renderMismatchYaml` (TAP YAML diagnostic block)
|
|
6
|
+
//
|
|
7
|
+
// These three functions are exactly what the deferred `Outcome.mismatch`
|
|
8
|
+
// normalization (plan C4) will unify. Pinning their current output byte-for-byte
|
|
9
|
+
// lets that refactor prove it preserved Human/TAP rendering — and makes the
|
|
10
|
+
// deliberate JUnit change (it currently renders via raw `Outcome.format`, unlike
|
|
11
|
+
// the RenderRescript-based Human/TAP) explicit and reviewable rather than silent.
|
|
12
|
+
//
|
|
13
|
+
// The fixtures cover all 10 mismatch kinds — the full structural variety the
|
|
14
|
+
// normalization must preserve: array expected/actual (Events/QueryRows/
|
|
15
|
+
// PublishedActions), the Error()/Ok() wrapping with a `None` actual
|
|
16
|
+
// (ErrorMismatch), the `key` extra + option rendering (StateMismatch), a
|
|
17
|
+
// single-sided actual (NoEventExpected), plain-string (non-JSON) sides
|
|
18
|
+
// (TranslateError), the `(id, value)` pair rendering (TodoMismatch), a nested
|
|
19
|
+
// JSON value (AppendConditionMismatch), and the `Throw` stack special case.
|
|
20
|
+
|
|
21
|
+
open JestGlobals
|
|
22
|
+
|
|
23
|
+
let evA = JSON.parseOrThrow(`{"TAG":"ProductAdded","_0":{"productId":"p1","name":"Widget"}}`)
|
|
24
|
+
let evB = JSON.parseOrThrow(`{"TAG":"ProductRenamed","_0":{"productId":"p1","name":"Gadget"}}`)
|
|
25
|
+
let stateActive = JSON.parseOrThrow(`{"status":"active"}`)
|
|
26
|
+
let stateArchived = JSON.parseOrThrow(`{"status":"archived"}`)
|
|
27
|
+
let appendExpected = JSON.parseOrThrow(`{"query":[]}`)
|
|
28
|
+
let appendActual = JSON.parseOrThrow(`{"query":[{"eventTypes":["ProductAdded"]}]}`)
|
|
29
|
+
|
|
30
|
+
type golden = {
|
|
31
|
+
name: string,
|
|
32
|
+
mismatch: Outcome.mismatch,
|
|
33
|
+
format: string,
|
|
34
|
+
human: string,
|
|
35
|
+
tap: string,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let goldens: array<golden> = [
|
|
39
|
+
{
|
|
40
|
+
name: "EventsMismatch",
|
|
41
|
+
mismatch: EventsMismatch({expected: [evA], actual: [evB]}),
|
|
42
|
+
format: "EventsMismatch:\n expected: [{\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n}]\n actual: [{\n \"TAG\": \"ProductRenamed\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Gadget\"\n }\n}]",
|
|
43
|
+
human: " expected: [ProductAdded({productId: \"p1\", name: \"Widget\"})]\n actual: [ProductRenamed({productId: \"p1\", name: \"Gadget\"})]",
|
|
44
|
+
tap: " kind: \"EventsMismatch\"\n expected: \"[ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"})]\"\n actual: \"[ProductRenamed({productId: \\\"p1\\\", name: \\\"Gadget\\\"})]\"",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "ErrorMismatch",
|
|
48
|
+
mismatch: ErrorMismatch({
|
|
49
|
+
expected: JSON.String("CategoryAlreadyExists"),
|
|
50
|
+
actual: None,
|
|
51
|
+
actualEvents: [evA],
|
|
52
|
+
}),
|
|
53
|
+
format: "ErrorMismatch:\n expected error: \"CategoryAlreadyExists\"\n actual error: (none)\n actual events: [{\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n}]",
|
|
54
|
+
human: " expected: Error(\"CategoryAlreadyExists\")\n actual: Ok([ProductAdded({productId: \"p1\", name: \"Widget\"})])",
|
|
55
|
+
tap: " kind: \"ErrorMismatch\"\n expected: \"Error(\\\"CategoryAlreadyExists\\\")\"\n actual: \"Ok([ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"})])\"",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "StateMismatch",
|
|
59
|
+
mismatch: StateMismatch({key: "p1", expected: Some(stateActive), actual: Some(stateArchived)}),
|
|
60
|
+
format: "StateMismatch (key: p1):\n expected: {\n \"status\": \"active\"\n}\n actual: {\n \"status\": \"archived\"\n}",
|
|
61
|
+
human: " key: \"p1\"\n expected: {status: \"active\"}\n actual: {status: \"archived\"}",
|
|
62
|
+
tap: " kind: \"StateMismatch\"\n key: \"p1\"\n expected: \"{status: \\\"active\\\"}\"\n actual: \"{status: \\\"archived\\\"}\"",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "NoEventExpected",
|
|
66
|
+
mismatch: NoEventExpected({actual: [evA]}),
|
|
67
|
+
format: "NoEventExpected: expected no events, got:\n [{\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n}]",
|
|
68
|
+
human: " expected no events\n actual: [ProductAdded({productId: \"p1\", name: \"Widget\"})]",
|
|
69
|
+
tap: " kind: \"NoEventExpected\"\n actual: \"[ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"})]\"",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "TranslateError",
|
|
73
|
+
mismatch: TranslateError({expected: "boom", actual: Some("kaboom")}),
|
|
74
|
+
format: "TranslateError:\n expected: boom\n actual: kaboom",
|
|
75
|
+
human: " expected: boom\n actual: kaboom",
|
|
76
|
+
tap: " kind: \"TranslateError\"\n expected: \"boom\"\n actual: \"kaboom\"",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "Throw",
|
|
80
|
+
mismatch: Throw({error: "unexpected", stack: "at foo"}),
|
|
81
|
+
format: "Throw: unexpected\nat foo",
|
|
82
|
+
human: " error: unexpected\nat foo",
|
|
83
|
+
tap: " kind: \"Throw\"\n error: \"unexpected\"\n stack: \"at foo\"",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "TodoMismatch",
|
|
87
|
+
mismatch: TodoMismatch({expected: [("case-1", evA)], actual: [("case-2", evB)]}),
|
|
88
|
+
format: "TodoMismatch:\n expected: [(case-1, {\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n})]\n actual: [(case-2, {\n \"TAG\": \"ProductRenamed\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Gadget\"\n }\n})]",
|
|
89
|
+
human: " expected: [(case-1, ProductAdded({productId: \"p1\", name: \"Widget\"}))]\n actual: [(case-2, ProductRenamed({productId: \"p1\", name: \"Gadget\"}))]",
|
|
90
|
+
tap: " kind: \"TodoMismatch\"\n expected: \"[(case-1, ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"}))]\"\n actual: \"[(case-2, ProductRenamed({productId: \\\"p1\\\", name: \\\"Gadget\\\"}))]\"",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "AppendConditionMismatch",
|
|
94
|
+
mismatch: AppendConditionMismatch({expected: appendExpected, actual: appendActual}),
|
|
95
|
+
format: "AppendConditionMismatch:\n expected: {\n \"query\": []\n}\n actual: {\n \"query\": [\n {\n \"eventTypes\": [\n \"ProductAdded\"\n ]\n }\n ]\n}",
|
|
96
|
+
human: " expected: {query: []}\n actual: {query: [{eventTypes: [\"ProductAdded\"]}]}",
|
|
97
|
+
tap: " kind: \"AppendConditionMismatch\"\n expected: \"{query: []}\"\n actual: \"{query: [{eventTypes: [\\\"ProductAdded\\\"]}]}\"",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "QueryRowsMismatch",
|
|
101
|
+
mismatch: QueryRowsMismatch({expected: [evA], actual: [evB]}),
|
|
102
|
+
format: "QueryRowsMismatch:\n expected: [{\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n}]\n actual: [{\n \"TAG\": \"ProductRenamed\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Gadget\"\n }\n}]",
|
|
103
|
+
human: " expected: [ProductAdded({productId: \"p1\", name: \"Widget\"})]\n actual: [ProductRenamed({productId: \"p1\", name: \"Gadget\"})]",
|
|
104
|
+
tap: " kind: \"QueryRowsMismatch\"\n expected: \"[ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"})]\"\n actual: \"[ProductRenamed({productId: \\\"p1\\\", name: \\\"Gadget\\\"})]\"",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "PublishedActionsMismatch",
|
|
108
|
+
mismatch: PublishedActionsMismatch({expected: [evA], actual: [evB]}),
|
|
109
|
+
format: "PublishedActionsMismatch:\n expected: [{\n \"TAG\": \"ProductAdded\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Widget\"\n }\n}]\n actual: [{\n \"TAG\": \"ProductRenamed\",\n \"_0\": {\n \"productId\": \"p1\",\n \"name\": \"Gadget\"\n }\n}]",
|
|
110
|
+
human: " expected: [ProductAdded({productId: \"p1\", name: \"Widget\"})]\n actual: [ProductRenamed({productId: \"p1\", name: \"Gadget\"})]",
|
|
111
|
+
tap: " kind: \"PublishedActionsMismatch\"\n expected: \"[ProductAdded({productId: \\\"p1\\\", name: \\\"Widget\\\"})]\"\n actual: \"[ProductRenamed({productId: \\\"p1\\\", name: \\\"Gadget\\\"})]\"",
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
describe("Formatter golden outputs (per-mismatch rendering)", () => {
|
|
116
|
+
goldens->Array.forEach(g => {
|
|
117
|
+
testSync(`${g.name} — Outcome.format`, () => expect(Outcome.format(g.mismatch))->toEqual(g.format))
|
|
118
|
+
testSync(`${g.name} — FormatterHuman.renderMismatch`, () =>
|
|
119
|
+
expect(FormatterHuman.renderMismatch(g.mismatch))->toEqual(g.human)
|
|
120
|
+
)
|
|
121
|
+
testSync(`${g.name} — FormatterTap.renderMismatchYaml`, () =>
|
|
122
|
+
expect(FormatterTap.renderMismatchYaml(g.mismatch))->toEqual(g.tap)
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
})
|