@reventlessdev/reventless-gwt 1.0.0-alpha.247 → 1.0.0-alpha.249
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 +21 -0
- package/README.md +1 -1
- package/package.json +7 -7
- package/src/.gwtignore +1 -1
- package/src/{Behavior_GWT.res → Decision_GWT.res} +110 -40
- package/src/{Behavior_GWT.res.mjs → Decision_GWT.res.mjs} +92 -42
- package/src/Delegate_GWT.res +1 -1
- package/src/EventMapping_GWT.res +7 -7
- package/src/EventMapping_GWT.res.mjs +3 -3
- package/src/Flow_GWT.res +14 -14
- package/src/Flow_GWT.res.mjs +6 -6
- package/src/Hint.res +1 -1
- package/src/Mapping_GWT.res +8 -8
- package/src/Mapping_GWT.res.mjs +6 -6
- package/src/Outcome.res +1 -1
- package/src/Query_GWT.res +1 -1
- package/src/RunnerHook.res +1 -1
- package/src/ScenarioRecorder.res +182 -0
- package/src/ScenarioRecorder.res.mjs +287 -0
- package/src/StateChange/ExternalAddCategorySlice.res +1 -1
- package/src/StateChange/WithFixtures.res +1 -1
- package/src/StateChange/WithManualOpen.res +1 -1
- package/tests/CallerTest.res +7 -7
- package/tests/CallerTest.res.mjs +8 -8
- package/tests/FlowAggregateGwtTest.res +6 -6
- package/tests/FlowAggregateGwtTest.res.mjs +6 -6
- package/tests/FlowCrossPluginGwtTest.res +8 -8
- package/tests/FlowCrossPluginGwtTest.res.mjs +8 -8
- package/tests/FlowGwtTest.res +4 -4
- package/tests/FlowGwtTest.res.mjs +4 -4
- package/tests/HintTest.res +2 -2
- package/tests/HintTest.res.mjs +1 -1
- package/tests/MappingGwtTest.res +5 -5
- package/tests/MappingGwtTest.res.mjs +6 -6
- package/tests/ScenarioRecorderTest.res +101 -0
- package/tests/ScenarioRecorderTest.res.mjs +163 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +3 -3
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +5 -5
- package/tests/StateChange/WithFixtures_GWT.res.mjs +5 -5
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +5 -5
- package/tests/StateChangeSliceAuthorizeGwtTest.res +10 -10
- package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +14 -14
- package/tests/StateChangeSliceGwtTest.res +9 -9
- package/tests/StateChangeSliceGwtTest.res.mjs +12 -12
- /package/src/StateChange/{ExternalAddCategorySlice_Behavior.res → ExternalAddCategorySlice_Decision.res} +0 -0
- /package/src/StateChange/{ExternalAddCategorySlice_Behavior.res.mjs → ExternalAddCategorySlice_Decision.res.mjs} +0 -0
- /package/src/StateChange/{WithFixtures_Behavior.res → WithFixtures_Decision.res} +0 -0
- /package/src/StateChange/{WithFixtures_Behavior.res.mjs → WithFixtures_Decision.res.mjs} +0 -0
- /package/src/StateChange/{WithManualOpen_Behavior.res → WithManualOpen_Decision.res} +0 -0
- /package/src/StateChange/{WithManualOpen_Behavior.res.mjs → WithManualOpen_Decision.res.mjs} +0 -0
package/src/Delegate_GWT.res
CHANGED
|
@@ -3,7 +3,7 @@ open ReventlessCore
|
|
|
3
3
|
// Delegate_GWT — GWT for ExtensionPoint mappings and Extension delegates.
|
|
4
4
|
//
|
|
5
5
|
// Both are pure input → published-actions translations and form the one
|
|
6
|
-
// cross-plugin
|
|
6
|
+
// cross-plugin behavioral seam (per `.claude/rules/app-developer.md`). Each
|
|
7
7
|
// mapping has TWO directions, and each adapter drives both:
|
|
8
8
|
//
|
|
9
9
|
// * an ExtensionPoint mapping turns an internal `Delegate` event into public
|
package/src/EventMapping_GWT.res
CHANGED
|
@@ -2,20 +2,20 @@ open ReventlessCore
|
|
|
2
2
|
|
|
3
3
|
// Stage 5 — `EventMapping_GWT` is now a thin alias over `Mapping_GWT`
|
|
4
4
|
// specialised for the Aggregate → Aggregate case. New tests should use
|
|
5
|
-
// `Mapping_GWT.Make` directly with `Mapping_GWT.
|
|
5
|
+
// `Mapping_GWT.Make` directly with `Mapping_GWT.FromDecision` adapters; this
|
|
6
6
|
// wrapper is retained for migration backward-compat with consumers that were
|
|
7
|
-
// already calling `EventMapping_GWT.Make(Source,
|
|
8
|
-
//
|
|
7
|
+
// already calling `EventMapping_GWT.Make(Source, SourceDecision, Target,
|
|
8
|
+
// TargetDecision, EventMapping)` before Stage 5 landed.
|
|
9
9
|
|
|
10
10
|
module Make = (
|
|
11
11
|
Source: Reventless.Aggregate.Spec,
|
|
12
|
-
|
|
12
|
+
SourceDecision: Decision.T with module Spec = Source,
|
|
13
13
|
Target: Reventless.Aggregate.Spec,
|
|
14
|
-
|
|
14
|
+
TargetDecision: Decision.T with module Spec = Target,
|
|
15
15
|
EventMapping: Reventless.EventMapping.T with module Source = Source and module Target = Target,
|
|
16
16
|
) => {
|
|
17
|
-
module AdaptedSource = Mapping_GWT.
|
|
18
|
-
module AdaptedTarget = Mapping_GWT.
|
|
17
|
+
module AdaptedSource = Mapping_GWT.FromDecision(Source, SourceDecision)
|
|
18
|
+
module AdaptedTarget = Mapping_GWT.FromDecision(Target, TargetDecision)
|
|
19
19
|
|
|
20
20
|
module BoundMapping = {
|
|
21
21
|
module Source = AdaptedSource
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import * as Mapping_GWT$ReventlessGwt from "./Mapping_GWT.res.mjs";
|
|
4
4
|
|
|
5
5
|
function Make(Source) {
|
|
6
|
-
return
|
|
7
|
-
let AdaptedSource = Mapping_GWT$ReventlessGwt.
|
|
8
|
-
let AdaptedTarget = Mapping_GWT$ReventlessGwt.
|
|
6
|
+
return SourceDecision => (Target => (TargetDecision => (EventMapping => {
|
|
7
|
+
let AdaptedSource = Mapping_GWT$ReventlessGwt.FromDecision(Source)(SourceDecision);
|
|
8
|
+
let AdaptedTarget = Mapping_GWT$ReventlessGwt.FromDecision(Target)(TargetDecision);
|
|
9
9
|
let map = EventMapping.map;
|
|
10
10
|
let BoundMapping = {
|
|
11
11
|
Source: undefined,
|
package/src/Flow_GWT.res
CHANGED
|
@@ -13,15 +13,15 @@ open ReventlessCore
|
|
|
13
13
|
// each step folds the slice's own `decide`/`evolve`/`collect`/`project` over
|
|
14
14
|
// the shared log. The log is JSON-erased (each step (de)serialises through its
|
|
15
15
|
// own spec's schema, exactly as the runtime routes a DCB event log), and each
|
|
16
|
-
// command step
|
|
16
|
+
// command step honors DCB tag filtering so downstream deciders never see
|
|
17
17
|
// phantom cross-entity events.
|
|
18
18
|
//
|
|
19
19
|
// The chain is built from per-step functors (one per slice), threaded with
|
|
20
20
|
// pipe-first so it reads top-to-bottom:
|
|
21
21
|
//
|
|
22
|
-
// module Place = Flow_GWT.CommandStep(PlaceOrder,
|
|
22
|
+
// module Place = Flow_GWT.CommandStep(PlaceOrder, PlaceOrder_Decision)
|
|
23
23
|
// module Auto = Flow_GWT.AutomationSlice(AutoShipOrder, AutoShipOrder_Automation)
|
|
24
|
-
// module Ship = Flow_GWT.CommandStep(ShipOrder,
|
|
24
|
+
// module Ship = Flow_GWT.CommandStep(ShipOrder, ShipOrder_Decision)
|
|
25
25
|
// module View = Flow_GWT.ViewStep(Orders, Orders_Projection)
|
|
26
26
|
//
|
|
27
27
|
// start
|
|
@@ -279,8 +279,8 @@ let thenBoundaryScopeResolves = async (
|
|
|
279
279
|
// -- CommandStep: a StateChangeSlice / Aggregate decider ---------------------
|
|
280
280
|
|
|
281
281
|
module CommandStep = (
|
|
282
|
-
Spec:
|
|
283
|
-
|
|
282
|
+
Spec: Decision_GWT.DecisionSpec,
|
|
283
|
+
Decision: Decision_GWT.Decision with module Spec := Spec,
|
|
284
284
|
) => {
|
|
285
285
|
let consumedDecoder = Reventless.DcbDecode.makeDecoder(Spec.consumedEventSchema)
|
|
286
286
|
let consumedEventTypes = consumedDecoder.eventTypes
|
|
@@ -335,8 +335,8 @@ module CommandStep = (
|
|
|
335
335
|
~payloadTagKeys,
|
|
336
336
|
)
|
|
337
337
|
let history = decodeMatching(s.log, consumedDecoder, query)
|
|
338
|
-
let state = history->Array.reduce(
|
|
339
|
-
switch
|
|
338
|
+
let state = history->Array.reduce(Decision.initialState, Decision.evolve)
|
|
339
|
+
switch Decision.decide(state, command) {
|
|
340
340
|
| Ok(events) => {
|
|
341
341
|
...s,
|
|
342
342
|
log: s.log->appendEvents(events, Spec.eventSchema),
|
|
@@ -390,8 +390,8 @@ module CommandStep = (
|
|
|
390
390
|
|
|
391
391
|
// -- AggregateCommandStep: an Aggregate decider partitioned by aggregate-ID --
|
|
392
392
|
//
|
|
393
|
-
// Mirror of `CommandStep` for `Reventless.
|
|
394
|
-
//
|
|
393
|
+
// Mirror of `CommandStep` for `Reventless.Decision.T` (the aggregate-flavor
|
|
394
|
+
// decision). Differences from the slice form:
|
|
395
395
|
// * No `consumedEvent` distinction — aggregates fold their own `event`
|
|
396
396
|
// stream; the decoder uses `Spec.eventSchema`.
|
|
397
397
|
// * No DCB tag inference — `~id` partitions the history. `givenEvents` and
|
|
@@ -404,8 +404,8 @@ module CommandStep = (
|
|
|
404
404
|
// histories isolated (a `Place` on `o2` does not see the events of `o1`).
|
|
405
405
|
|
|
406
406
|
module AggregateCommandStep = (
|
|
407
|
-
Spec:
|
|
408
|
-
|
|
407
|
+
Spec: Decision_GWT.AggregateSpec,
|
|
408
|
+
Decision: Reventless.Decision.T with module Spec = Spec,
|
|
409
409
|
) => {
|
|
410
410
|
let aggregateDecoder = Reventless.DcbDecode.makeDecoder(Spec.eventSchema)
|
|
411
411
|
|
|
@@ -422,8 +422,8 @@ module AggregateCommandStep = (
|
|
|
422
422
|
let whenCommand = async (flowP: flow, ~id: string, command: Spec.command) => {
|
|
423
423
|
let s = await flowP
|
|
424
424
|
let history = decodeAggregateMatching(s.log, ~id, aggregateDecoder)
|
|
425
|
-
let state = history->Array.reduce(
|
|
426
|
-
switch
|
|
425
|
+
let state = history->Array.reduce(Decision.initialState, Decision.evolve)
|
|
426
|
+
switch Decision.decide(state, command) {
|
|
427
427
|
| Ok(events) => {
|
|
428
428
|
...s,
|
|
429
429
|
log: s.log->appendAggregateEvents(~id, events, Spec.eventSchema),
|
|
@@ -717,7 +717,7 @@ module ExtensionPointStep = (M: EPMapping.Mapping) => {
|
|
|
717
717
|
// Aggregate-style EP mappings address the source aggregate's identity via
|
|
718
718
|
// the first arg (e.g. catalog Product → ProductBecameAvailable uses it as
|
|
719
719
|
// `productId`). DCB upstreams don't set `lastAggregateId` and fall back to
|
|
720
|
-
// the synthetic `gwt-id` so existing DCB Flow tests keep their
|
|
720
|
+
// the synthetic `gwt-id` so existing DCB Flow tests keep their behavior.
|
|
721
721
|
let runMapping = async (sourceId: string, events: array<M.Delegate.event>): array<
|
|
722
722
|
M.ExtensionPoint.event,
|
|
723
723
|
> =>
|
package/src/Flow_GWT.res.mjs
CHANGED
|
@@ -171,7 +171,7 @@ async function thenBoundaryScopeResolves(flowP, name, slices) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function CommandStep(Spec) {
|
|
174
|
-
return
|
|
174
|
+
return Decision => {
|
|
175
175
|
let consumedDecoder = DcbDecode$Reventless.makeDecoder(Spec.consumedEventSchema);
|
|
176
176
|
let consumedEventTypes = consumedDecoder.eventTypes;
|
|
177
177
|
let givenEvents = async (flowP, events) => {
|
|
@@ -207,8 +207,8 @@ function CommandStep(Spec) {
|
|
|
207
207
|
let s = await flowP;
|
|
208
208
|
let query = DcbTag$Reventless.buildQueryFromCommand(consumedEventTypes, Spec.commandSchema, command, tagKeysByEventType, crossPartitionTagKeys, payloadTagKeys);
|
|
209
209
|
let history = decodeMatching(s.log, consumedDecoder, query);
|
|
210
|
-
let state = Stdlib_Array.reduce(history,
|
|
211
|
-
let events =
|
|
210
|
+
let state = Stdlib_Array.reduce(history, Decision.initialState, Decision.evolve);
|
|
211
|
+
let events = Decision.decide(state, command);
|
|
212
212
|
if (events.TAG !== "Ok") {
|
|
213
213
|
return {
|
|
214
214
|
log: s.log,
|
|
@@ -288,7 +288,7 @@ function CommandStep(Spec) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
function AggregateCommandStep(Spec) {
|
|
291
|
-
return
|
|
291
|
+
return Decision => {
|
|
292
292
|
let aggregateDecoder = DcbDecode$Reventless.makeDecoder(Spec.eventSchema);
|
|
293
293
|
let givenEvents = async (flowP, id, events) => {
|
|
294
294
|
let s = await flowP;
|
|
@@ -306,8 +306,8 @@ function AggregateCommandStep(Spec) {
|
|
|
306
306
|
let whenCommand = async (flowP, id, command) => {
|
|
307
307
|
let s = await flowP;
|
|
308
308
|
let history = decodeAggregateMatching(s.log, id, aggregateDecoder);
|
|
309
|
-
let state = Stdlib_Array.reduce(history,
|
|
310
|
-
let events =
|
|
309
|
+
let state = Stdlib_Array.reduce(history, Decision.initialState, Decision.evolve);
|
|
310
|
+
let events = Decision.decide(state, command);
|
|
311
311
|
if (events.TAG !== "Ok") {
|
|
312
312
|
return {
|
|
313
313
|
log: s.log,
|
package/src/Hint.res
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// See `docs/analysis/given-when-then-specifications.md` §3.2 (locus table)
|
|
6
6
|
// and §3.3 (JSON `hint` field).
|
|
7
7
|
//
|
|
8
|
-
// `locus` is a dotted pointer like "
|
|
8
|
+
// `locus` is a dotted pointer like "CategoryDecision.decide" filled in by
|
|
9
9
|
// the DSL at `then*` call time when the slice module name is in scope, else a
|
|
10
10
|
// generic pointer. A `StateMismatch` names its fold, so a view's points at
|
|
11
11
|
// `project` rather than `evolve`.
|
package/src/Mapping_GWT.res
CHANGED
|
@@ -3,7 +3,7 @@ open ReventlessCore
|
|
|
3
3
|
// Stage 5 — cross-pattern mapping GWT.
|
|
4
4
|
//
|
|
5
5
|
// Generalises `EventMapping_GWT` so both the source and target of a mapping
|
|
6
|
-
// can be an Aggregate (
|
|
6
|
+
// can be an Aggregate (Decision) or a StateChangeSlice. Covers all four
|
|
7
7
|
// producer/consumer combinations: Aggr→Aggr, Aggr→DCB, DCB→Aggr, DCB→DCB.
|
|
8
8
|
// See `docs/plans/done/reventless-gwt.md` Stage 5 for the migration plan.
|
|
9
9
|
//
|
|
@@ -54,20 +54,20 @@ module type Mapping = {
|
|
|
54
54
|
|
|
55
55
|
// -- Adapter functors --------------------------------------------------------
|
|
56
56
|
|
|
57
|
-
// Adapter: any `Aggregate.Spec` + matching `
|
|
57
|
+
// Adapter: any `Aggregate.Spec` + matching `Decision.T` becomes a `GwtSource`.
|
|
58
58
|
// Aggregates evolve their own events, so `consumedEvent = event`.
|
|
59
|
-
module
|
|
59
|
+
module FromDecision = (
|
|
60
60
|
Spec: Reventless.Aggregate.Spec,
|
|
61
|
-
|
|
61
|
+
Decision: Decision.T with module Spec = Spec,
|
|
62
62
|
) => {
|
|
63
63
|
let name = Spec.name
|
|
64
64
|
module Id = Spec.Id
|
|
65
|
-
type state =
|
|
66
|
-
let initialState =
|
|
65
|
+
type state = Decision.state
|
|
66
|
+
let initialState = Decision.initialState
|
|
67
67
|
|
|
68
68
|
type consumedEvent = Spec.event
|
|
69
69
|
let consumedEventSchema = Spec.eventSchema
|
|
70
|
-
let evolve =
|
|
70
|
+
let evolve = Decision.evolve
|
|
71
71
|
|
|
72
72
|
type command = Spec.command
|
|
73
73
|
let commandSchema = Spec.commandSchema
|
|
@@ -78,7 +78,7 @@ module FromBehavior = (
|
|
|
78
78
|
type error = Spec.error
|
|
79
79
|
let errorSchema = Spec.errorSchema
|
|
80
80
|
|
|
81
|
-
let decide =
|
|
81
|
+
let decide = Decision.decide
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Adapter: a StateChangeSlice spec becomes a `GwtSource`/`GwtTarget`. DCB
|
package/src/Mapping_GWT.res.mjs
CHANGED
|
@@ -9,17 +9,17 @@ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
|
|
|
9
9
|
import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
|
|
10
10
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
11
11
|
|
|
12
|
-
function
|
|
13
|
-
return
|
|
12
|
+
function FromDecision(Spec) {
|
|
13
|
+
return Decision => ({
|
|
14
14
|
name: Spec.name,
|
|
15
15
|
Id: Spec.Id,
|
|
16
|
-
initialState:
|
|
16
|
+
initialState: Decision.initialState,
|
|
17
17
|
consumedEventSchema: Spec.eventSchema,
|
|
18
|
-
evolve:
|
|
18
|
+
evolve: Decision.evolve,
|
|
19
19
|
commandSchema: Spec.commandSchema,
|
|
20
20
|
eventSchema: Spec.eventSchema,
|
|
21
21
|
errorSchema: Spec.errorSchema,
|
|
22
|
-
decide:
|
|
22
|
+
decide: Decision.decide
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -313,7 +313,7 @@ function Make(M) {
|
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
export {
|
|
316
|
-
|
|
316
|
+
FromDecision,
|
|
317
317
|
FromStateChangeSlice,
|
|
318
318
|
Make,
|
|
319
319
|
}
|
package/src/Outcome.res
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// shape and §3.3 for the JSON rendering. Stage 7 adds sury-aware rendering and
|
|
9
9
|
// structural `fieldDiff`. Stage 4 added `AppendConditionMismatch`.
|
|
10
10
|
|
|
11
|
-
// The function whose fold produced a state: a
|
|
11
|
+
// The function whose fold produced a state: a decision's `evolve`, or a view's
|
|
12
12
|
// `project`. Absent means `Evolve`, so a harness that does not say keeps its hint.
|
|
13
13
|
type fold = Evolve | Project
|
|
14
14
|
|
package/src/Query_GWT.res
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// `subIdConfig`), so both kinds of consumers use the same combinator set.
|
|
13
13
|
// Adapter functors `FromReadModel` and `FromStateViewSlice` lift their
|
|
14
14
|
// respective specs into the shared shape — mirroring how `Mapping_GWT` lifts
|
|
15
|
-
// `Aggregate.Spec`/`StateChangeSlice.Spec` via `
|
|
15
|
+
// `Aggregate.Spec`/`StateChangeSlice.Spec` via `FromDecision`/`FromStateChangeSlice`.
|
|
16
16
|
|
|
17
17
|
// -- Unified spec shape ------------------------------------------------------
|
|
18
18
|
|
package/src/RunnerHook.res
CHANGED
|
@@ -30,7 +30,7 @@ type sink = {
|
|
|
30
30
|
) => unit,
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The currently-registered sink, if any. `None` → plain Jest
|
|
33
|
+
// The currently-registered sink, if any. `None` → plain Jest behavior.
|
|
34
34
|
let current: ref<option<sink>> = ref(None)
|
|
35
35
|
|
|
36
36
|
let register = (s: sink) => current := Some(s)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Decision scenarios as they run, written as sidecars `check-lifecycle` reads.
|
|
2
|
+
//
|
|
3
|
+
// The PPX writes a `.gwt.json` sidecar from a `_GWT.res` file's source, so it can only name what
|
|
4
|
+
// the source spells out: a literal constructor in a step. A suite written against a binding (a
|
|
5
|
+
// trait's conformance suite, whose `B.request(…)` is whatever the host's binding builds) names
|
|
6
|
+
// nothing in its source, and the host's file that registers it has no `describe` of its own, so
|
|
7
|
+
// no sidecar is written and `check-lifecycle` sees none of its scenarios. The run has the values,
|
|
8
|
+
// and the spec's schemas name them; this records them.
|
|
9
|
+
//
|
|
10
|
+
// Only with `REVENTLESS_EMIT_SIDECAR` set in the test process, the switch the PPX reads at build
|
|
11
|
+
// time; otherwise every call is a no-op. A test file whose sidecar the PPX wrote is left alone:
|
|
12
|
+
// read from source, it carries values and ids a run cannot.
|
|
13
|
+
|
|
14
|
+
type entry = {kind: string, element: string}
|
|
15
|
+
|
|
16
|
+
type scenario = {
|
|
17
|
+
title: string,
|
|
18
|
+
given: array<entry>,
|
|
19
|
+
when_: array<entry>,
|
|
20
|
+
then_: array<entry>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let enabled = () =>
|
|
24
|
+
switch NodeProcess.env->Dict.get("REVENTLESS_EMIT_SIDECAR") {
|
|
25
|
+
| Some("1" | "true" | "TRUE") => true
|
|
26
|
+
| _ => false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The scenario of the test running now, if one is being recorded.
|
|
30
|
+
let current: ref<option<scenario>> = ref(None)
|
|
31
|
+
|
|
32
|
+
// The test file an external runner says it is running; Jest says it itself.
|
|
33
|
+
let fileOverride: ref<option<string>> = ref(None)
|
|
34
|
+
|
|
35
|
+
/** Names the test file an external runner (registered through `RunnerHook`) is about to run, so
|
|
36
|
+
what its tests record lands beside it. */
|
|
37
|
+
let setFile = (path: string) => fileOverride := Some(path)
|
|
38
|
+
|
|
39
|
+
// The running test's file under Jest, while a test runs.
|
|
40
|
+
let jestTestPath: unit => option<string> = %raw(`function () {
|
|
41
|
+
try {
|
|
42
|
+
const state = globalThis.expect && globalThis.expect.getState && globalThis.expect.getState();
|
|
43
|
+
return state && typeof state.testPath === "string" ? state.testPath : undefined;
|
|
44
|
+
} catch (_) { return undefined; }
|
|
45
|
+
}`)
|
|
46
|
+
|
|
47
|
+
let testFile = () =>
|
|
48
|
+
switch fileOverride.contents {
|
|
49
|
+
| Some(f) => Some(f)
|
|
50
|
+
| None => jestTestPath()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// What has been recorded in this process, per sidecar path: the spec it names and its scenarios.
|
|
54
|
+
let recorded: Map.t<string, (string, array<scenario>)> = Map.make()
|
|
55
|
+
|
|
56
|
+
// Per test file, the specs recorded from it in order, the first owning `<File>_GWT.gwt.json`.
|
|
57
|
+
let specsOf: Map.t<string, array<string>> = Map.make()
|
|
58
|
+
|
|
59
|
+
// Sidecar paths found written by the PPX: never written over.
|
|
60
|
+
let foreign: Set.t<string> = Set.make()
|
|
61
|
+
|
|
62
|
+
// `…/X_GWT.res.mjs` (or `.res`, `.mjs`, `.js`) → `…/X_GWT`.
|
|
63
|
+
let baseOf = (file: string) =>
|
|
64
|
+
[".res.mjs", ".res.js", ".res", ".mjs", ".js"]->Array.reduce(file, (f, ext) =>
|
|
65
|
+
f->String.endsWith(ext)
|
|
66
|
+
? f->String.slice(~start=0, ~end=String.length(f) - String.length(ext))
|
|
67
|
+
: f
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
let sidecarFor = (~file: string, ~spec: string) => {
|
|
71
|
+
let specs = specsOf->Map.get(file)->Option.getOr([])
|
|
72
|
+
if !(specs->Array.includes(spec)) {
|
|
73
|
+
specs->Array.push(spec)
|
|
74
|
+
specsOf->Map.set(file, specs)
|
|
75
|
+
}
|
|
76
|
+
specs->Array.indexOf(spec) == 0 ? `${baseOf(file)}.gwt.json` : `${baseOf(file)}.${spec}.gwt.json`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Whether a sidecar at `path` is the PPX's: present and not marked recorded.
|
|
80
|
+
let writtenByPpx = (path: string) =>
|
|
81
|
+
switch NodeFs.existsSync(path) {
|
|
82
|
+
| false => false
|
|
83
|
+
| true =>
|
|
84
|
+
switch NodeFs.readFileSync(path)->JSON.parseOrThrow {
|
|
85
|
+
| Object(d) => d->Dict.get("recorded") != Some(Boolean(true))
|
|
86
|
+
| _ => true
|
|
87
|
+
| exception _ => true
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let entryJson = (e: entry) => JSON.Object(
|
|
92
|
+
Dict.fromArray([
|
|
93
|
+
("kind", JSON.String(e.kind)),
|
|
94
|
+
("element", JSON.String(e.element)),
|
|
95
|
+
("values", JSON.Array([])),
|
|
96
|
+
]),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
let write = (~path: string, ~file: string, ~spec: string, scenarios: array<scenario>) => {
|
|
100
|
+
let json = JSON.Object(
|
|
101
|
+
Dict.fromArray([
|
|
102
|
+
("specName", JSON.String(`${spec} (recorded)`)),
|
|
103
|
+
// What `check-lifecycle` takes the file's component from.
|
|
104
|
+
("stem", JSON.String(spec)),
|
|
105
|
+
("file", JSON.String(file)),
|
|
106
|
+
("componentKind", JSON.String("Decision")),
|
|
107
|
+
("recorded", JSON.Boolean(true)),
|
|
108
|
+
(
|
|
109
|
+
"scenarios",
|
|
110
|
+
JSON.Array(
|
|
111
|
+
scenarios->Array.map(s => JSON.Object(
|
|
112
|
+
Dict.fromArray([
|
|
113
|
+
("title", JSON.String(s.title)),
|
|
114
|
+
("given", JSON.Array(s.given->Array.map(entryJson))),
|
|
115
|
+
("when", JSON.Array(s.when_->Array.map(entryJson))),
|
|
116
|
+
("then", JSON.Array(s.then_->Array.map(entryJson))),
|
|
117
|
+
]),
|
|
118
|
+
)),
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
]),
|
|
122
|
+
)
|
|
123
|
+
NodeFs.writeFileSync(path, JSON.stringifyAny(json, ~space=2)->Option.getOr("{}") ++ "\n")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Runs one test's `body`, recording its steps when recording is on, and adds the scenario to
|
|
127
|
+
its file's sidecar. `spec` is the spec the test runs against, the component the sidecar names. */
|
|
128
|
+
let around = (~spec: string, title: string, body: unit => 'outcome): 'outcome =>
|
|
129
|
+
if !enabled() {
|
|
130
|
+
body()
|
|
131
|
+
} else {
|
|
132
|
+
current := Some({title, given: [], when_: [], then_: []})
|
|
133
|
+
let finish = () => {
|
|
134
|
+
let scenario = current.contents
|
|
135
|
+
current := None
|
|
136
|
+
switch (scenario, testFile()) {
|
|
137
|
+
| (Some(s), Some(file)) =>
|
|
138
|
+
let path = sidecarFor(~file, ~spec)
|
|
139
|
+
if !(foreign->Set.has(path)) && !(recorded->Map.has(path)) && writtenByPpx(path) {
|
|
140
|
+
foreign->Set.add(path)
|
|
141
|
+
}
|
|
142
|
+
if !(foreign->Set.has(path)) {
|
|
143
|
+
let (_, scenarios) = recorded->Map.get(path)->Option.getOr((spec, []))
|
|
144
|
+
let scenarios = scenarios->Array.filter(o => o.title != s.title)->Array.concat([s])
|
|
145
|
+
recorded->Map.set(path, (spec, scenarios))
|
|
146
|
+
write(~path, ~file, ~spec, scenarios)
|
|
147
|
+
}
|
|
148
|
+
| _ => ()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
switch body() {
|
|
152
|
+
| outcome =>
|
|
153
|
+
finish()
|
|
154
|
+
outcome
|
|
155
|
+
| exception e =>
|
|
156
|
+
finish()
|
|
157
|
+
throw(e)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Whether a test is being recorded now: the steps name what they get only then. */
|
|
162
|
+
let recording = () => current.contents->Option.isSome
|
|
163
|
+
|
|
164
|
+
let push = (pick: scenario => array<entry>, entries: array<entry>) =>
|
|
165
|
+
current.contents->Option.forEach(s => entries->Array.forEach(e => pick(s)->Array.push(e)))
|
|
166
|
+
|
|
167
|
+
/** The history a command is decided on, by event name. */
|
|
168
|
+
let given = (names: array<string>) =>
|
|
169
|
+
push(s => s.given, names->Array.map(element => {kind: "event", element}))
|
|
170
|
+
|
|
171
|
+
/** The command a test sends. */
|
|
172
|
+
let whenCommand = (name: string) => push(s => s.when_, [{kind: "command", element: name}])
|
|
173
|
+
|
|
174
|
+
/** What a test expects: these events. */
|
|
175
|
+
let thenEvents = (names: array<string>) =>
|
|
176
|
+
push(s => s.then_, names->Array.map(element => {kind: "event", element}))
|
|
177
|
+
|
|
178
|
+
/** What a test expects: this error. */
|
|
179
|
+
let thenError = (name: string) => push(s => s.then_, [{kind: "error", element: name}])
|
|
180
|
+
|
|
181
|
+
/** What a test expects: no event. */
|
|
182
|
+
let thenNoEvent = () => push(s => s.then_, [{kind: "noEvent", element: ""}])
|