@reventlessdev/reventless-gwt 1.0.0-alpha.142 → 1.0.0-alpha.144
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 +15 -0
- package/package.json +7 -6
- package/rescript.json +1 -0
- package/src/Behavior_GWT.res +15 -2
- package/src/Behavior_GWT.res.mjs +6 -6
- package/src/LocalHost.res +15 -19
- package/tests/LocalHostIntegration.res +5 -9
- package/tests/LocalHostIntegration.res.mjs +1 -1
- package/tests/LocalHostTest.res +21 -26
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 1.0.0-alpha.144 (2026-08-01)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **gwt:** compare produced events by their encoded wire form ([f773062](https://github.com/ReventlessDev/reventless-core/commit/f773062bc8ea18bf12e5611fcbad81aa4f1bd1b2))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 1.0.0-alpha.143 (2026-07-31)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 1.0.0-alpha.142 (2026-07-30)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @reventlessdev/reventless-gwt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-gwt",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.144",
|
|
4
4
|
"description": "Given-When-Then DSLs and test harness for Reventless slice testing",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0-alpha.4",
|
|
17
|
-
"@reventlessdev/
|
|
18
|
-
"@reventlessdev/
|
|
19
|
-
"@reventlessdev/
|
|
20
|
-
"@reventlessdev/
|
|
21
|
-
"@reventlessdev/
|
|
17
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.0",
|
|
18
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.197",
|
|
19
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.115",
|
|
20
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
21
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.90"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"rescript": "12.3.0",
|
package/rescript.json
CHANGED
package/src/Behavior_GWT.res
CHANGED
|
@@ -153,10 +153,22 @@ module AssertionCore = (Spec: CoreSpec) => {
|
|
|
153
153
|
)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// Events are compared by their **encoded (wire) form**, not by ReScript
|
|
157
|
+
// structural equality. For an event-sourced system the serialized event *is*
|
|
158
|
+
// its identity: two events that reverse-convert to the same JSON are the same
|
|
159
|
+
// event. Raw `==` is stricter than that — it distinguishes an optional field
|
|
160
|
+
// that is present-but-`undefined` (what a decider's `deliveryWindow: ?x`
|
|
161
|
+
// passthrough emits when `x` is `None`) from one whose key is absent (what a
|
|
162
|
+
// test literal that omits the field produces), even though sury drops a `None`
|
|
163
|
+
// optional either way and both hit the log identically. Comparing on `encEvents`
|
|
164
|
+
// unifies that spurious difference, so an optional event field can be omitted in
|
|
165
|
+
// the expectation instead of spelled out as `?None`. It is a strictly weaker
|
|
166
|
+
// equality: equal values still encode equally, so nothing that passed can start
|
|
167
|
+
// failing — it only stops the wire-invisible key asymmetry from failing a test.
|
|
156
168
|
let compareEvents = (events, expectedEvents) =>
|
|
157
169
|
if errors.contents->Array.length > 0 {
|
|
158
170
|
unexpectedError(events)
|
|
159
|
-
} else if events == expectedEvents {
|
|
171
|
+
} else if events->encEvents == expectedEvents->encEvents {
|
|
160
172
|
Outcome.pass
|
|
161
173
|
} else {
|
|
162
174
|
Outcome.fail(
|
|
@@ -211,7 +223,8 @@ module AssertionCore = (Spec: CoreSpec) => {
|
|
|
211
223
|
}),
|
|
212
224
|
)
|
|
213
225
|
| Some(_) =>
|
|
214
|
-
|
|
226
|
+
// Encoded comparison, for the same reason as `compareEvents` above.
|
|
227
|
+
if events->encEvents == expectedEvents->encEvents {
|
|
215
228
|
Outcome.pass
|
|
216
229
|
} else {
|
|
217
230
|
Outcome.fail(
|
package/src/Behavior_GWT.res.mjs
CHANGED
|
@@ -66,7 +66,7 @@ function AssertionCore(Spec) {
|
|
|
66
66
|
let compareEvents = (events, expectedEvents) => {
|
|
67
67
|
if (errors.contents.length !== 0) {
|
|
68
68
|
return unexpectedError(events);
|
|
69
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
69
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
70
70
|
return Outcome$ReventlessGwt.pass;
|
|
71
71
|
} else {
|
|
72
72
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -120,7 +120,7 @@ function AssertionCore(Spec) {
|
|
|
120
120
|
actual: Message$ReventlessCore.encode(actual$1, Spec.errorSchema),
|
|
121
121
|
actualEvents: events.map(encEvent)
|
|
122
122
|
});
|
|
123
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
123
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
124
124
|
return Outcome$ReventlessGwt.pass;
|
|
125
125
|
} else {
|
|
126
126
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -180,7 +180,7 @@ function Make(Spec) {
|
|
|
180
180
|
actual: Message$ReventlessCore.encode(actual$1, Spec.errorSchema),
|
|
181
181
|
actualEvents: events.map(encEvent)
|
|
182
182
|
});
|
|
183
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
183
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
184
184
|
return Outcome$ReventlessGwt.pass;
|
|
185
185
|
} else {
|
|
186
186
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -281,7 +281,7 @@ function Make(Spec) {
|
|
|
281
281
|
return o;
|
|
282
282
|
} else if (errors.contents.length !== 0) {
|
|
283
283
|
return unexpectedError(events);
|
|
284
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
284
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
285
285
|
return Outcome$ReventlessGwt.pass;
|
|
286
286
|
} else {
|
|
287
287
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -413,7 +413,7 @@ function MakeFromAggregate(Spec) {
|
|
|
413
413
|
let compareEvents = (events, expectedEvents) => {
|
|
414
414
|
if (errors.contents.length !== 0) {
|
|
415
415
|
return unexpectedError(events);
|
|
416
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
416
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
417
417
|
return Outcome$ReventlessGwt.pass;
|
|
418
418
|
} else {
|
|
419
419
|
return Outcome$ReventlessGwt.fail({
|
|
@@ -467,7 +467,7 @@ function MakeFromAggregate(Spec) {
|
|
|
467
467
|
actual: Message$ReventlessCore.encode(actual$1, Spec.errorSchema),
|
|
468
468
|
actualEvents: events.map(encEvent)
|
|
469
469
|
});
|
|
470
|
-
} else if (Primitive_object.equal(events, expectedEvents)) {
|
|
470
|
+
} else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
|
|
471
471
|
return Outcome$ReventlessGwt.pass;
|
|
472
472
|
} else {
|
|
473
473
|
return Outcome$ReventlessGwt.fail({
|
package/src/LocalHost.res
CHANGED
|
@@ -13,16 +13,6 @@
|
|
|
13
13
|
// path via dynamic import, so a missing local platform is a caller concern, not a
|
|
14
14
|
// compile-time coupling.
|
|
15
15
|
|
|
16
|
-
@module("node:url") external pathToFileURL: string => {"href": string} = "pathToFileURL"
|
|
17
|
-
@module("node:fs") external existsSync: string => bool = "existsSync"
|
|
18
|
-
@module("node:fs") external readFileSync: (string, string) => string = "readFileSync"
|
|
19
|
-
@module("node:path") external join: (string, string) => string = "join"
|
|
20
|
-
@module("node:path") external dirname: string => string = "dirname"
|
|
21
|
-
|
|
22
|
-
type nodeRequire
|
|
23
|
-
@module("node:module") external createRequire: string => nodeRequire = "createRequire"
|
|
24
|
-
@send external requireResolve: (nodeRequire, string) => string = "resolve"
|
|
25
|
-
|
|
26
16
|
let dynamicImport: string => promise<'a> = %raw(`(u) => import(u)`)
|
|
27
17
|
|
|
28
18
|
// Monotonic cache-buster so repeated loads in one process (a watch session
|
|
@@ -35,7 +25,7 @@ let dynamicImport: string => promise<'a> = %raw(`(u) => import(u)`)
|
|
|
35
25
|
let counter = ref(0)
|
|
36
26
|
let bustedUrl = (absolutePath: string): string => {
|
|
37
27
|
counter := counter.contents + 1
|
|
38
|
-
pathToFileURL(absolutePath)["href"] ++ "?t=" ++ Int.toString(counter.contents)
|
|
28
|
+
NodeUrl.pathToFileURL(absolutePath)["href"] ++ "?t=" ++ Int.toString(counter.contents)
|
|
39
29
|
}
|
|
40
30
|
|
|
41
31
|
type pluginRef = {name: string, modulePath: string, packageDir: string}
|
|
@@ -65,7 +55,7 @@ let strField = (json, key) =>
|
|
|
65
55
|
json->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))->Option.flatMap(JSON.Decode.string)
|
|
66
56
|
|
|
67
57
|
let readJson = path =>
|
|
68
|
-
try Some(readFileSync(path
|
|
58
|
+
try Some(NodeFs.readFileSync(path)->JSON.parseOrThrow) catch {
|
|
69
59
|
| _ => None
|
|
70
60
|
}
|
|
71
61
|
|
|
@@ -73,11 +63,14 @@ let readJson = path =>
|
|
|
73
63
|
// The precedence itself lives in `Reventless.PluginName.resolve`; this only
|
|
74
64
|
// reads the two raw fields with the local node bindings.
|
|
75
65
|
let derivePluginName = (~pluginSrcDir: string): string => {
|
|
76
|
-
let pluginJson = join(pluginSrcDir, "plugin.json")
|
|
66
|
+
let pluginJson = NodePath.join([pluginSrcDir, "plugin.json"])
|
|
77
67
|
let pluginJsonName =
|
|
78
|
-
existsSync(pluginJson)
|
|
68
|
+
NodeFs.existsSync(pluginJson)
|
|
69
|
+
? readJson(pluginJson)->Option.flatMap(j => strField(j, "name"))
|
|
70
|
+
: None
|
|
79
71
|
let packageJsonName =
|
|
80
|
-
readJson(join(dirname(pluginSrcDir), "package.json"))
|
|
72
|
+
readJson(NodePath.join([NodePath.dirname(pluginSrcDir), "package.json"]))
|
|
73
|
+
->Option.flatMap(j => strField(j, "name"))
|
|
81
74
|
Reventless.PluginName.resolve(~pluginJsonName, ~packageJsonName)
|
|
82
75
|
}
|
|
83
76
|
|
|
@@ -85,9 +78,9 @@ let derivePluginName = (~pluginSrcDir: string): string => {
|
|
|
85
78
|
// compiled composition root and pair each with its framework plugin name.
|
|
86
79
|
let discover = (~packageDirs: array<string>): array<pluginRef> =>
|
|
87
80
|
packageDirs->Array.filterMap(dir => {
|
|
88
|
-
let srcDir = join(dir, "src")
|
|
89
|
-
let modulePath = join(srcDir, "Plugin.res.mjs")
|
|
90
|
-
existsSync(modulePath)
|
|
81
|
+
let srcDir = NodePath.join([dir, "src"])
|
|
82
|
+
let modulePath = NodePath.join([srcDir, "Plugin.res.mjs"])
|
|
83
|
+
NodeFs.existsSync(modulePath)
|
|
91
84
|
? Some({name: derivePluginName(~pluginSrcDir=srcDir), modulePath, packageDir: dir})
|
|
92
85
|
: None
|
|
93
86
|
})
|
|
@@ -99,7 +92,10 @@ let discover = (~packageDirs: array<string>): array<pluginRef> =>
|
|
|
99
92
|
// there — callers then skip platform-dependent features (dead-code / graph).
|
|
100
93
|
let localPlatformSpecifier = "@reventlessdev/reventless-local/src/Platform.res.mjs"
|
|
101
94
|
let resolveLocalPlatform = (~fromPackageDir: string): option<string> =>
|
|
102
|
-
try Some(
|
|
95
|
+
try Some(
|
|
96
|
+
NodeModule.createRequire(NodePath.join([fromPackageDir, "package.json"]))
|
|
97
|
+
->NodeModule.requireResolve(localPlatformSpecifier),
|
|
98
|
+
) catch {
|
|
103
99
|
| _ => None
|
|
104
100
|
}
|
|
105
101
|
|
|
@@ -18,15 +18,11 @@
|
|
|
18
18
|
@module("node:assert/strict") external deepEqual: ('a, 'a) => unit = "deepEqual"
|
|
19
19
|
@module("node:assert/strict") external ok: (bool, ~message: string=?) => unit = "ok"
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
let repoRoot = join([here, "..", "..", ".."]) // reventless-gwt/test → repo root
|
|
27
|
-
let catalogDir = join([repoRoot, "examples", "online-shop-aggregates", "catalog"])
|
|
28
|
-
let orderingDir = join([repoRoot, "examples", "online-shop-aggregates", "ordering"])
|
|
29
|
-
let platformPath = join([repoRoot, "reventless", "reventless-local", "src", "Platform.res.mjs"])
|
|
21
|
+
let here = NodePath.dirname(NodeUrl.fileURLToPath(%raw(`import.meta.url`)))
|
|
22
|
+
let repoRoot = NodePath.join([here, "..", "..", ".."]) // reventless-gwt/test → repo root
|
|
23
|
+
let catalogDir = NodePath.join([repoRoot, "examples", "online-shop-aggregates", "catalog"])
|
|
24
|
+
let orderingDir = NodePath.join([repoRoot, "examples", "online-shop-aggregates", "ordering"])
|
|
25
|
+
let platformPath = NodePath.join([repoRoot, "reventless", "local", "src", "Platform.res.mjs"])
|
|
30
26
|
|
|
31
27
|
let structureFor = (g: LocalHost.graph, name) =>
|
|
32
28
|
g.structures->Array.find(((n, _)) => n == name)->Option.map(((_, s)) => s)
|
|
@@ -15,7 +15,7 @@ let catalogDir = Nodepath.join(repoRoot, "examples", "online-shop-aggregates", "
|
|
|
15
15
|
|
|
16
16
|
let orderingDir = Nodepath.join(repoRoot, "examples", "online-shop-aggregates", "ordering");
|
|
17
17
|
|
|
18
|
-
let platformPath = Nodepath.join(repoRoot, "reventless", "
|
|
18
|
+
let platformPath = Nodepath.join(repoRoot, "reventless", "local", "src", "Platform.res.mjs");
|
|
19
19
|
|
|
20
20
|
function structureFor(g, name) {
|
|
21
21
|
return Stdlib_Option.map(g.structures.find(param => param[0] === name), param => param[1]);
|
package/tests/LocalHostTest.res
CHANGED
|
@@ -10,16 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
open JestGlobals
|
|
12
12
|
|
|
13
|
-
@module("node:os") external tmpdir: unit => string = "tmpdir"
|
|
14
|
-
@module("node:path") external join: (string, string) => string = "join"
|
|
15
|
-
|
|
16
|
-
type mkdirOpts = {recursive: bool}
|
|
17
|
-
@module("node:fs/promises")
|
|
18
|
-
external mkdir: (string, mkdirOpts) => promise<Nullable.t<string>> = "mkdir"
|
|
19
|
-
@module("node:fs/promises") external writeFile: (string, string) => promise<unit> = "writeFile"
|
|
20
|
-
type rmOpts = {recursive: bool, force: bool}
|
|
21
|
-
@module("node:fs/promises") external rm: (string, rmOpts) => promise<unit> = "rm"
|
|
22
|
-
|
|
23
13
|
describe("LocalHost.packageNameToPluginName", () => {
|
|
24
14
|
testPromise("PascalCases scoped / dashed / underscored package names", async () => {
|
|
25
15
|
expect(LocalHost.packageNameToPluginName("@scope/my-catalog"))->toBe("MyCatalog")
|
|
@@ -34,27 +24,32 @@ describe("LocalHost name derivation + discovery", () => {
|
|
|
34
24
|
testPromise(
|
|
35
25
|
"derivePluginName prefers plugin.json then PascalCase(package.json); discover skips non-plugins",
|
|
36
26
|
async () => {
|
|
37
|
-
let root = join(tmpdir(), "reventless-localhost-test")
|
|
38
|
-
let _ = await rm(root, {recursive: true, force: true})
|
|
27
|
+
let root = NodePath.join([NodeOs.tmpdir(), "reventless-localhost-test"])
|
|
28
|
+
let _ = await NodeFs.Promises.rm(root, {recursive: true, force: true})
|
|
39
29
|
|
|
40
30
|
// a: explicit plugin.json name + a compiled composition root.
|
|
41
|
-
let a = join(root, "a")
|
|
42
|
-
let aSrc = join(a, "src")
|
|
43
|
-
let _ = await mkdir(aSrc, {recursive: true})
|
|
44
|
-
let _ =
|
|
45
|
-
|
|
46
|
-
let _ =
|
|
31
|
+
let a = NodePath.join([root, "a"])
|
|
32
|
+
let aSrc = NodePath.join([a, "src"])
|
|
33
|
+
let _ = await NodeFs.Promises.mkdir(aSrc, {recursive: true})
|
|
34
|
+
let _ =
|
|
35
|
+
await NodeFs.Promises.writeFile(NodePath.join([a, "package.json"]), `{"name":"@x/a-pkg"}`)
|
|
36
|
+
let _ =
|
|
37
|
+
await NodeFs.Promises.writeFile(NodePath.join([aSrc, "plugin.json"]), `{"name":"Catalog"}`)
|
|
38
|
+
let _ = await NodeFs.Promises.writeFile(NodePath.join([aSrc, "Plugin.res.mjs"]), "")
|
|
47
39
|
|
|
48
40
|
// b: no plugin.json → name falls back to PascalCase(package.json name).
|
|
49
|
-
let b = join(root, "b")
|
|
50
|
-
let bSrc = join(b, "src")
|
|
51
|
-
let _ = await mkdir(bSrc, {recursive: true})
|
|
52
|
-
let _ = await writeFile(
|
|
53
|
-
|
|
41
|
+
let b = NodePath.join([root, "b"])
|
|
42
|
+
let bSrc = NodePath.join([b, "src"])
|
|
43
|
+
let _ = await NodeFs.Promises.mkdir(bSrc, {recursive: true})
|
|
44
|
+
let _ = await NodeFs.Promises.writeFile(
|
|
45
|
+
NodePath.join([b, "package.json"]),
|
|
46
|
+
`{"name":"@scope/my-ordering"}`,
|
|
47
|
+
)
|
|
48
|
+
let _ = await NodeFs.Promises.writeFile(NodePath.join([bSrc, "Plugin.res.mjs"]), "")
|
|
54
49
|
|
|
55
50
|
// c: has src/ but no Plugin.res.mjs → discover must skip it.
|
|
56
|
-
let c = join(root, "c")
|
|
57
|
-
let _ = await mkdir(join(c, "src"), {recursive: true})
|
|
51
|
+
let c = NodePath.join([root, "c"])
|
|
52
|
+
let _ = await NodeFs.Promises.mkdir(NodePath.join([c, "src"]), {recursive: true})
|
|
58
53
|
|
|
59
54
|
expect(LocalHost.derivePluginName(~pluginSrcDir=aSrc))->toBe("Catalog")
|
|
60
55
|
expect(LocalHost.derivePluginName(~pluginSrcDir=bSrc))->toBe("MyOrdering")
|
|
@@ -65,7 +60,7 @@ describe("LocalHost name derivation + discovery", () => {
|
|
|
65
60
|
expect(first.modulePath->String.endsWith("Plugin.res.mjs"))->toBe(true)
|
|
66
61
|
expect(first.packageDir)->toBe(a)
|
|
67
62
|
|
|
68
|
-
let _ = await rm(root, {recursive: true, force: true})
|
|
63
|
+
let _ = await NodeFs.Promises.rm(root, {recursive: true, force: true})
|
|
69
64
|
},
|
|
70
65
|
)
|
|
71
66
|
})
|