@reventlessdev/reventless-gwt 1.0.0-alpha.76 → 1.0.0-alpha.78
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 +7 -7
- package/src/Behavior_GWT.res +125 -159
- package/src/Behavior_GWT.res.mjs +156 -59
- package/src/BuildClassifier.res +15 -5
- package/src/BuildClassifier.res.mjs +3 -3
- package/src/Cli.res +157 -29
- package/src/Cli.res.mjs +138 -24
- package/src/Collector.res +2 -2
- package/src/Collector.res.mjs +2 -2
- 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/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/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/PlatformScan.res +14 -10
- package/src/PlatformScan.res.mjs +14 -13
- package/tests/BuildClassifierTest.res +39 -0
- package/tests/BuildClassifierTest.res.mjs +44 -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/src/PlatformScan.res.mjs
CHANGED
|
@@ -83,19 +83,20 @@ async function walk(dir, acc) {
|
|
|
83
83
|
let pkg = inspectDir(dir);
|
|
84
84
|
if (pkg !== undefined) {
|
|
85
85
|
acc.push(pkg);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
} else {
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = await Promises.readdir(dir, {
|
|
90
|
+
withFileTypes: true
|
|
91
|
+
});
|
|
92
|
+
} catch (exn) {
|
|
93
|
+
entries = [];
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
|
|
96
|
+
let entry = entries[i];
|
|
97
|
+
if (entry.isDirectory() && !ignoreNames.includes(entry.name)) {
|
|
98
|
+
await walk(Nodepath.join(dir, entry.name), acc);
|
|
99
|
+
}
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
return acc;
|
|
@@ -80,6 +80,45 @@ describe("BuildClassifier", () => {
|
|
|
80
80
|
expect(fails.contents->Array.length)->toEqual(0)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
+
testPromise("errors emitted before the first Parsed line still settle to onFail (B3)", async () => {
|
|
84
|
+
// A crashed toolchain / pnpm resolution failure prints an error block with no
|
|
85
|
+
// preceding "Parsed …" line. The old `compiling && hasError` guard left this
|
|
86
|
+
// pinned forever; now `hasError` alone settles it.
|
|
87
|
+
let oks = ref(0)
|
|
88
|
+
let fails = ref([])
|
|
89
|
+
let feed = BuildClassifier.make({
|
|
90
|
+
onStart: () => (),
|
|
91
|
+
onOk: _ms => oks := oks.contents + 1,
|
|
92
|
+
onFail: msg => fails := Array.concat(fails.contents, [msg]),
|
|
93
|
+
})
|
|
94
|
+
feed("Syntax error: unexpected token")
|
|
95
|
+
feed(" at Foo.res:3:1")
|
|
96
|
+
await delay(550)
|
|
97
|
+
expect(oks.contents)->toEqual(0)
|
|
98
|
+
expect(fails.contents->Array.length)->toEqual(1)
|
|
99
|
+
expect((fails.contents->Array.getUnsafe(0))->String.includes("Syntax error"))->toEqual(true)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
testPromise("a multi-error build keeps error blocks past the old 20-line cap (B3)", async () => {
|
|
103
|
+
let fails = ref([])
|
|
104
|
+
let feed = BuildClassifier.make({
|
|
105
|
+
onStart: () => (),
|
|
106
|
+
onOk: _ms => (),
|
|
107
|
+
onFail: msg => fails := Array.concat(fails.contents, [msg]),
|
|
108
|
+
})
|
|
109
|
+
feed("Parsed 1 source files")
|
|
110
|
+
feed("We've found a bug for you!")
|
|
111
|
+
// 30 filler lines push a distinctive later error past the old 20-line window.
|
|
112
|
+
for i in 1 to 30 {
|
|
113
|
+
feed("noise line " ++ Int.toString(i))
|
|
114
|
+
}
|
|
115
|
+
feed("Found 2 errors")
|
|
116
|
+
await delay(550)
|
|
117
|
+
expect(fails.contents->Array.length)->toEqual(1)
|
|
118
|
+
// The late "Found 2 errors" line survives (dropped under the old cap of 20).
|
|
119
|
+
expect((fails.contents->Array.getUnsafe(0))->String.includes("Found 2 errors"))->toEqual(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
83
122
|
testPromise("strips ANSI colour codes before matching the finish line", async () => {
|
|
84
123
|
let oks = ref(0)
|
|
85
124
|
let feed = BuildClassifier.make({
|
|
@@ -109,6 +109,50 @@ globalThis.describe("BuildClassifier", () => {
|
|
|
109
109
|
globalThis.expect(oks.contents).toEqual(1);
|
|
110
110
|
globalThis.expect(fails.contents.length).toEqual(0);
|
|
111
111
|
});
|
|
112
|
+
globalThis.test("errors emitted before the first Parsed line still settle to onFail (B3)", async () => {
|
|
113
|
+
let oks = {
|
|
114
|
+
contents: 0
|
|
115
|
+
};
|
|
116
|
+
let fails = {
|
|
117
|
+
contents: []
|
|
118
|
+
};
|
|
119
|
+
let feed = BuildClassifier$ReventlessGwt.make(undefined, {
|
|
120
|
+
onStart: () => {},
|
|
121
|
+
onOk: _ms => {
|
|
122
|
+
oks.contents = oks.contents + 1 | 0;
|
|
123
|
+
},
|
|
124
|
+
onFail: msg => {
|
|
125
|
+
fails.contents = fails.contents.concat([msg]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
feed("Syntax error: unexpected token");
|
|
129
|
+
feed(" at Foo.res:3:1");
|
|
130
|
+
await delay(550);
|
|
131
|
+
globalThis.expect(oks.contents).toEqual(0);
|
|
132
|
+
globalThis.expect(fails.contents.length).toEqual(1);
|
|
133
|
+
globalThis.expect(fails.contents[0].includes("Syntax error")).toEqual(true);
|
|
134
|
+
});
|
|
135
|
+
globalThis.test("a multi-error build keeps error blocks past the old 20-line cap (B3)", async () => {
|
|
136
|
+
let fails = {
|
|
137
|
+
contents: []
|
|
138
|
+
};
|
|
139
|
+
let feed = BuildClassifier$ReventlessGwt.make(undefined, {
|
|
140
|
+
onStart: () => {},
|
|
141
|
+
onOk: _ms => {},
|
|
142
|
+
onFail: msg => {
|
|
143
|
+
fails.contents = fails.contents.concat([msg]);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
feed("Parsed 1 source files");
|
|
147
|
+
feed("We've found a bug for you!");
|
|
148
|
+
for (let i = 1; i <= 30; ++i) {
|
|
149
|
+
feed("noise line " + i.toString());
|
|
150
|
+
}
|
|
151
|
+
feed("Found 2 errors");
|
|
152
|
+
await delay(550);
|
|
153
|
+
globalThis.expect(fails.contents.length).toEqual(1);
|
|
154
|
+
globalThis.expect(fails.contents[0].includes("Found 2 errors")).toEqual(true);
|
|
155
|
+
});
|
|
112
156
|
globalThis.test("strips ANSI colour codes before matching the finish line", async () => {
|
|
113
157
|
let oks = {
|
|
114
158
|
contents: 0
|
|
@@ -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
|
+
})
|