@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Behavior_GWT.res +125 -159
  4. package/src/Behavior_GWT.res.mjs +156 -59
  5. package/src/Bind.res.mjs +2 -2
  6. package/src/BuildClassifier.res +15 -5
  7. package/src/BuildClassifier.res.mjs +3 -3
  8. package/src/Cancellation.res +6 -0
  9. package/src/Cancellation.res.mjs +3 -2
  10. package/src/ChildProcess.res +19 -1
  11. package/src/ChildProcess.res.mjs +16 -1
  12. package/src/Cli.res +260 -26
  13. package/src/Cli.res.mjs +227 -29
  14. package/src/Collector.res +13 -2
  15. package/src/Collector.res.mjs +8 -5
  16. package/src/ComponentMeta.res +16 -43
  17. package/src/ComponentMeta.res.mjs +12 -41
  18. package/src/Discovery.res +19 -19
  19. package/src/Discovery.res.mjs +5 -9
  20. package/src/DomainGraph.res +0 -0
  21. package/src/DomainGraph.res.mjs +0 -0
  22. package/src/Filter.res +11 -2
  23. package/src/Filter.res.mjs +10 -3
  24. package/src/FormatterHuman.res +14 -43
  25. package/src/FormatterHuman.res.mjs +21 -28
  26. package/src/FormatterJson.res +12 -5
  27. package/src/FormatterJson.res.mjs +8 -6
  28. package/src/FormatterTap.res +13 -52
  29. package/src/FormatterTap.res.mjs +25 -43
  30. package/src/FormatterVsCode.res +4 -19
  31. package/src/FormatterVsCode.res.mjs +4 -23
  32. package/src/JestBind.res +1 -1
  33. package/src/JestBind.res.mjs +2 -2
  34. package/src/LocalHost.res +11 -27
  35. package/src/LocalHost.res.mjs +6 -34
  36. package/src/MismatchRender.res +72 -0
  37. package/src/MismatchRender.res.mjs +127 -0
  38. package/src/Outcome.res +0 -64
  39. package/src/Outcome.res.mjs +0 -47
  40. package/src/PlatformRunner.res +27 -6
  41. package/src/PlatformRunner.res.mjs +27 -6
  42. package/src/PlatformScan.res +14 -10
  43. package/src/PlatformScan.res.mjs +14 -13
  44. package/src/ProcessManager.res +51 -7
  45. package/src/ProcessManager.res.mjs +43 -4
  46. package/src/RunWorker.res +24 -0
  47. package/src/RunWorker.res.mjs +16 -0
  48. package/src/Watch.res +46 -30
  49. package/src/Watch.res.mjs +34 -32
  50. package/src/WatcherProbe.res +11 -3
  51. package/src/WatcherProbe.res.mjs +15 -2
  52. package/src/Worker.res +24 -0
  53. package/src/Worker.res.mjs +9 -0
  54. package/tests/BuildClassifierTest.res +39 -0
  55. package/tests/BuildClassifierTest.res.mjs +44 -0
  56. package/tests/CliRunEntryTest.res +57 -0
  57. package/tests/CliRunEntryTest.res.mjs +65 -0
  58. package/tests/CliWatchScopeTest.res +62 -0
  59. package/tests/CliWatchScopeTest.res.mjs +95 -0
  60. package/tests/ComponentMetaTest.res +15 -0
  61. package/tests/ComponentMetaTest.res.mjs +14 -0
  62. package/tests/DomainGraphTest.res +2 -2
  63. package/tests/FlowAggregateGwtTest.res +2 -0
  64. package/tests/FlowAggregateGwtTest.res.mjs +4 -0
  65. package/tests/FormatterGoldenTest.res +125 -0
  66. package/tests/FormatterGoldenTest.res.mjs +162 -0
  67. package/tests/MappingGwtTest.res +2 -0
  68. package/tests/MappingGwtTest.res.mjs +4 -0
  69. package/tests/WatchDebounceTest.res +35 -7
  70. 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.to == to && e.kind == kind)
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.to == to && e.kind == kind)->Option.flatMap(e => e.label)
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
+ })