@reventlessdev/reventless-gwt 1.0.0-alpha.246 → 1.0.0-alpha.248

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 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.248 (2026-10-19)
7
+
8
+ ### Features
9
+
10
+ * **gwt:** record behaviour scenarios as they run, for suites whose source names none ([256b253](https://github.com/ReventlessDev/reventless-core/commit/256b253153ec043105c38ac742a1a3efbde48c87))
11
+
12
+
13
+ # 1.0.0-alpha.247 (2026-10-17)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/reventless-gwt
16
+
17
+
18
+
19
+
20
+
6
21
  # 1.0.0-alpha.246 (2026-10-16)
7
22
 
8
23
  * feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.246",
3
+ "version": "1.0.0-alpha.248",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -15,16 +15,16 @@
15
15
  "dependencies": {
16
16
  "sury": "11.0.0",
17
17
  "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
18
+ "@reventlessdev/reventless-core": "3.0.0-alpha.300",
18
19
  "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.197",
19
21
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
20
- "@reventlessdev/reventless-core": "3.0.0-alpha.298",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.195",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.167"
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.169"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
26
26
  "sury-ppx": "11.0.0",
27
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.102"
27
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.103"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "rescript": "^12.3.1"
@@ -443,7 +443,10 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
443
443
 
444
444
  let describe = JestBind.describe
445
445
  let todo = JestBind.todo
446
- let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
446
+ let test = (name, body) =>
447
+ JestBind.test(~slice=Spec.name, name, () =>
448
+ ScenarioRecorder.around(~spec=Spec.name, name, body)
449
+ )
447
450
 
448
451
  let currentState = consumed => consumed->Array.reduce(Behavior.initialState, Behavior.evolve)
449
452
 
@@ -614,7 +617,27 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
614
617
  }
615
618
 
616
619
  let givenEvents = consumed => consumed
617
- let whenCmd = (history, cmd) => history->exec(cmd)
620
+ let whenCmd = (history, cmd) => {
621
+ if ScenarioRecorder.recording() {
622
+ ScenarioRecorder.given(
623
+ history->Array.map(e =>
624
+ e->Message.encode(Spec.consumedEventSchema)->Message.variantNameOfJson
625
+ ),
626
+ )
627
+ ScenarioRecorder.whenCommand(Guard.commandName(cmd))
628
+ }
629
+ history->exec(cmd)
630
+ }
631
+ // What a test expects, named for the recorded sidecar.
632
+ let expects = (~events=[], ~error=?) =>
633
+ if ScenarioRecorder.recording() {
634
+ ScenarioRecorder.thenEvents(
635
+ events->Array.map(e => e->Core.encEvent->Message.variantNameOfJson),
636
+ )
637
+ error->Option.forEach(err =>
638
+ ScenarioRecorder.thenError(err->Core.encError->Message.variantNameOfJson)
639
+ )
640
+ }
618
641
 
619
642
  let checkAppendCondition = (): option<Outcome.outcome> =>
620
643
  switch appendConditionFailure.contents {
@@ -631,37 +654,49 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
631
654
 
632
655
  // The append-condition footgun is surfaced before the shared comparison core
633
656
  // runs; `thenAppends*` below bypass it deliberately.
634
- let thenEvents = (events, expectedEvents) =>
657
+ let thenEvents = (events, expectedEvents) => {
658
+ expects(~events=expectedEvents)
635
659
  switch checkAppendCondition() {
636
660
  | Some(o) => o
637
661
  | None => Core.compareEvents(events, expectedEvents)
638
662
  }
663
+ }
639
664
 
640
665
  let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent])
641
666
 
642
- let thenNoEvent = events =>
667
+ let thenNoEvent = events => {
668
+ if ScenarioRecorder.recording() {
669
+ ScenarioRecorder.thenNoEvent()
670
+ }
643
671
  switch checkAppendCondition() {
644
672
  | Some(o) => o
645
673
  | None => Core.compareNoEvent(events)
646
674
  }
675
+ }
647
676
 
648
- let thenError = (events, expectedError) =>
677
+ let thenError = (events, expectedError) => {
678
+ expects(~error=expectedError)
649
679
  switch checkAppendCondition() {
650
680
  | Some(o) => o
651
681
  | None => Core.matchesError(events, [], expectedError)
652
682
  }
683
+ }
653
684
 
654
- let thenEventWithError = (events, expectedEvent, expectedError) =>
685
+ let thenEventWithError = (events, expectedEvent, expectedError) => {
686
+ expects(~events=[expectedEvent], ~error=expectedError)
655
687
  switch checkAppendCondition() {
656
688
  | Some(o) => o
657
689
  | None => Core.matchesError(events, [expectedEvent], expectedError)
658
690
  }
691
+ }
659
692
 
660
- let thenEventsWithError = (events, expectedEvents, expectedError) =>
693
+ let thenEventsWithError = (events, expectedEvents, expectedError) => {
694
+ expects(~events=expectedEvents, ~error=expectedError)
661
695
  switch checkAppendCondition() {
662
696
  | Some(o) => o
663
697
  | None => Core.matchesError(events, expectedEvents, expectedError)
664
698
  }
699
+ }
665
700
 
666
701
  let thenAppendsConditionedOn = (_events, expectedQuery: Reventless.DcbTag.query) =>
667
702
  switch derivedCondition.contents {
@@ -773,7 +808,10 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
773
808
  module Spec = Spec
774
809
 
775
810
  let describe = JestBind.describe
776
- let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
811
+ let test = (name, body) =>
812
+ JestBind.test(~slice=Spec.name, name, () =>
813
+ ScenarioRecorder.around(~spec=Spec.name, name, body)
814
+ )
777
815
 
778
816
  let currentState = events => events->Array.reduce(Behavior.initialState, Behavior.evolve)
779
817
 
@@ -809,7 +847,23 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
809
847
  }
810
848
 
811
849
  let givenEvents = events => events
812
- let whenCmd = (history, cmd) => history->exec(cmd)
850
+ let whenCmd = (history, cmd) => {
851
+ if ScenarioRecorder.recording() {
852
+ ScenarioRecorder.given(history->Array.map(e => e->Core.encEvent->Message.variantNameOfJson))
853
+ ScenarioRecorder.whenCommand(Guard.commandName(cmd))
854
+ }
855
+ history->exec(cmd)
856
+ }
857
+ // What a test expects, named for the recorded sidecar.
858
+ let expects = (~events=[], ~error=?) =>
859
+ if ScenarioRecorder.recording() {
860
+ ScenarioRecorder.thenEvents(
861
+ events->Array.map(e => e->Core.encEvent->Message.variantNameOfJson),
862
+ )
863
+ error->Option.forEach(err =>
864
+ ScenarioRecorder.thenError(err->Core.encError->Message.variantNameOfJson)
865
+ )
866
+ }
813
867
 
814
868
  // The aggregate flavour has no append-condition footgun, so the shared core's
815
869
  // comparisons are exposed directly once a refusal has been ruled out; only
@@ -819,22 +873,38 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
819
873
  | Some(o) => o
820
874
  | None => compare(events)
821
875
  }
822
- let thenEvents = (events, expected) => events->unlessRefused(Core.compareEvents(_, expected))
823
- let thenCompareEvents = (events, expected, cmp) =>
876
+ let thenEvents = (events, expected) => {
877
+ expects(~events=expected)
878
+ events->unlessRefused(Core.compareEvents(_, expected))
879
+ }
880
+ let thenCompareEvents = (events, expected, cmp) => {
881
+ expects(~events=expected)
824
882
  events->unlessRefused(Core.compareEventsWith(_, expected, cmp))
883
+ }
825
884
  let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent])
826
885
  let thenCompareEvent = (events, expectedEvent, cmp) =>
827
886
  thenCompareEvents(events, [expectedEvent], cmp)
828
- let thenNoEvent = events => events->unlessRefused(Core.compareNoEvent)
887
+ let thenNoEvent = events => {
888
+ if ScenarioRecorder.recording() {
889
+ ScenarioRecorder.thenNoEvent()
890
+ }
891
+ events->unlessRefused(Core.compareNoEvent)
892
+ }
829
893
 
830
- let thenError = (events, expectedError) =>
894
+ let thenError = (events, expectedError) => {
895
+ expects(~error=expectedError)
831
896
  events->unlessRefused(Core.matchesError(_, [], expectedError))
897
+ }
832
898
 
833
- let thenEventWithError = (events, expectedEvent, expectedError) =>
899
+ let thenEventWithError = (events, expectedEvent, expectedError) => {
900
+ expects(~events=[expectedEvent], ~error=expectedError)
834
901
  events->unlessRefused(Core.matchesError(_, [expectedEvent], expectedError))
902
+ }
835
903
 
836
- let thenEventsWithError = (events, expectedEvents, expectedError) =>
904
+ let thenEventsWithError = (events, expectedEvents, expectedError) => {
905
+ expects(~events=expectedEvents, ~error=expectedError)
837
906
  events->unlessRefused(Core.matchesError(_, expectedEvents, expectedError))
907
+ }
838
908
 
839
909
  let thenRefused = events =>
840
910
  Guard.thenRefused(
@@ -15,6 +15,7 @@ import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
15
15
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
16
16
  import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
17
17
  import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
18
+ import * as ScenarioRecorder$ReventlessGwt from "./ScenarioRecorder.res.mjs";
18
19
  import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
19
20
 
20
21
  function roleNames(roles) {
@@ -317,7 +318,7 @@ function AssertionCore(Spec) {
317
318
 
318
319
  function Make(Spec) {
319
320
  return Behavior => {
320
- let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
321
+ let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, () => ScenarioRecorder$ReventlessGwt.around(Spec.name, name, body));
321
322
  let errors = {
322
323
  contents: []
323
324
  };
@@ -494,6 +495,10 @@ function Make(Spec) {
494
495
  let queryTagsTotal = q => Stdlib_Array.reduce(q, 0, (acc, qi) => acc + Stdlib_Option.mapOr(qi.tags, 0, t => t.length) | 0);
495
496
  let givenEvents = consumed => consumed;
496
497
  let whenCmd = (history, cmd) => {
498
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
499
+ ScenarioRecorder$ReventlessGwt.given(history.map(e => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(e, Spec.consumedEventSchema))));
500
+ ScenarioRecorder$ReventlessGwt.whenCommand(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(cmd, commandSchema)));
501
+ }
497
502
  errors.contents = [];
498
503
  appendConditionFailure.contents = undefined;
499
504
  let query = DcbTag$Reventless.buildQueryFromCommand(consumedEventTypes, Spec.commandSchema, cmd, tagKeysByEventType, crossPartitionTagKeys, payloadTagKeys);
@@ -529,6 +534,13 @@ function Make(Spec) {
529
534
  errors.contents = [events._0];
530
535
  return [];
531
536
  };
537
+ let expects = (eventsOpt, error) => {
538
+ let events = eventsOpt !== undefined ? eventsOpt : [];
539
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
540
+ ScenarioRecorder$ReventlessGwt.thenEvents(events.map(e => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(e, Spec.eventSchema))));
541
+ return Stdlib_Option.forEach(error, err => ScenarioRecorder$ReventlessGwt.thenError(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(err, Spec.errorSchema))));
542
+ }
543
+ };
532
544
  let checkAppendCondition = () => {
533
545
  let m = appendConditionFailure.contents;
534
546
  if (m !== undefined) {
@@ -555,6 +567,7 @@ function Make(Spec) {
555
567
  }
556
568
  };
557
569
  let thenEvents = (events, expectedEvents) => {
570
+ expects(expectedEvents, undefined);
558
571
  let o = checkAppendCondition();
559
572
  if (o !== undefined) {
560
573
  return o;
@@ -572,6 +585,9 @@ function Make(Spec) {
572
585
  };
573
586
  let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent]);
574
587
  let thenNoEvent = events => {
588
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
589
+ ScenarioRecorder$ReventlessGwt.thenNoEvent();
590
+ }
575
591
  let o = checkAppendCondition();
576
592
  if (o !== undefined) {
577
593
  return o;
@@ -587,6 +603,7 @@ function Make(Spec) {
587
603
  }
588
604
  };
589
605
  let thenError = (events, expectedError) => {
606
+ expects(undefined, Primitive_option.some(expectedError));
590
607
  let o = checkAppendCondition();
591
608
  if (o !== undefined) {
592
609
  return o;
@@ -595,6 +612,7 @@ function Make(Spec) {
595
612
  }
596
613
  };
597
614
  let thenEventWithError = (events, expectedEvent, expectedError) => {
615
+ expects([expectedEvent], Primitive_option.some(expectedError));
598
616
  let o = checkAppendCondition();
599
617
  if (o !== undefined) {
600
618
  return o;
@@ -603,6 +621,7 @@ function Make(Spec) {
603
621
  }
604
622
  };
605
623
  let thenEventsWithError = (events, expectedEvents, expectedError) => {
624
+ expects(expectedEvents, Primitive_option.some(expectedError));
606
625
  let o = checkAppendCondition();
607
626
  if (o !== undefined) {
608
627
  return o;
@@ -676,7 +695,7 @@ function Make(Spec) {
676
695
 
677
696
  function MakeFromAggregate(Spec) {
678
697
  return Behavior => {
679
- let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
698
+ let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, () => ScenarioRecorder$ReventlessGwt.around(Spec.name, name, body));
680
699
  let errors = {
681
700
  contents: []
682
701
  };
@@ -810,6 +829,10 @@ function MakeFromAggregate(Spec) {
810
829
  }));
811
830
  let givenEvents = events => events;
812
831
  let whenCmd = (history, cmd) => {
832
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
833
+ ScenarioRecorder$ReventlessGwt.given(history.map(e => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(e, Spec.eventSchema))));
834
+ ScenarioRecorder$ReventlessGwt.whenCommand(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(cmd, commandSchema)));
835
+ }
813
836
  errors.contents = [];
814
837
  if (refuses(history, cmd)) {
815
838
  return [];
@@ -821,6 +844,13 @@ function MakeFromAggregate(Spec) {
821
844
  errors.contents = [events._0];
822
845
  return [];
823
846
  };
847
+ let expects = (eventsOpt, error) => {
848
+ let events = eventsOpt !== undefined ? eventsOpt : [];
849
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
850
+ ScenarioRecorder$ReventlessGwt.thenEvents(events.map(e => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(e, Spec.eventSchema))));
851
+ return Stdlib_Option.forEach(error, err => ScenarioRecorder$ReventlessGwt.thenError(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(err, Spec.errorSchema))));
852
+ }
853
+ };
824
854
  let unlessRefused = (events, compare) => {
825
855
  let o = unexpectedRefusal();
826
856
  if (o !== undefined) {
@@ -829,38 +859,58 @@ function MakeFromAggregate(Spec) {
829
859
  return compare(events);
830
860
  }
831
861
  };
832
- let thenEvents = (events, expected) => unlessRefused(events, __x => {
833
- if (errors.contents.length !== 0) {
834
- return unexpectedError(__x);
835
- } else if (Primitive_object.equal(__x.map(encEvent), expected.map(encEvent))) {
836
- return Outcome$ReventlessGwt.pass;
837
- } else {
838
- return Outcome$ReventlessGwt.fail({
839
- TAG: "EventsMismatch",
840
- expected: expected.map(encEvent),
841
- actual: __x.map(encEvent)
842
- });
843
- }
844
- });
845
- let thenCompareEvents = (events, expected, cmp) => unlessRefused(events, __x => {
846
- if (errors.contents.length !== 0) {
847
- return unexpectedError(__x);
848
- } else if (__x.length === expected.length && Stdlib_Array.zip(__x, expected).every(param => cmp(param[0], param[1]))) {
849
- return Outcome$ReventlessGwt.pass;
850
- } else {
851
- return Outcome$ReventlessGwt.fail({
852
- TAG: "EventsMismatch",
853
- expected: expected.map(encEvent),
854
- actual: __x.map(encEvent)
855
- });
856
- }
857
- });
862
+ let thenEvents = (events, expected) => {
863
+ expects(expected, undefined);
864
+ return unlessRefused(events, __x => {
865
+ if (errors.contents.length !== 0) {
866
+ return unexpectedError(__x);
867
+ } else if (Primitive_object.equal(__x.map(encEvent), expected.map(encEvent))) {
868
+ return Outcome$ReventlessGwt.pass;
869
+ } else {
870
+ return Outcome$ReventlessGwt.fail({
871
+ TAG: "EventsMismatch",
872
+ expected: expected.map(encEvent),
873
+ actual: __x.map(encEvent)
874
+ });
875
+ }
876
+ });
877
+ };
878
+ let thenCompareEvents = (events, expected, cmp) => {
879
+ expects(expected, undefined);
880
+ return unlessRefused(events, __x => {
881
+ if (errors.contents.length !== 0) {
882
+ return unexpectedError(__x);
883
+ } else if (__x.length === expected.length && Stdlib_Array.zip(__x, expected).every(param => cmp(param[0], param[1]))) {
884
+ return Outcome$ReventlessGwt.pass;
885
+ } else {
886
+ return Outcome$ReventlessGwt.fail({
887
+ TAG: "EventsMismatch",
888
+ expected: expected.map(encEvent),
889
+ actual: __x.map(encEvent)
890
+ });
891
+ }
892
+ });
893
+ };
858
894
  let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent]);
859
895
  let thenCompareEvent = (events, expectedEvent, cmp) => thenCompareEvents(events, [expectedEvent], cmp);
860
- let thenNoEvent = events => unlessRefused(events, compareNoEvent);
861
- let thenError = (events, expectedError) => unlessRefused(events, __x => matchesError(__x, [], expectedError));
862
- let thenEventWithError = (events, expectedEvent, expectedError) => unlessRefused(events, __x => matchesError(__x, [expectedEvent], expectedError));
863
- let thenEventsWithError = (events, expectedEvents, expectedError) => unlessRefused(events, __x => matchesError(__x, expectedEvents, expectedError));
896
+ let thenNoEvent = events => {
897
+ if (ScenarioRecorder$ReventlessGwt.recording()) {
898
+ ScenarioRecorder$ReventlessGwt.thenNoEvent();
899
+ }
900
+ return unlessRefused(events, compareNoEvent);
901
+ };
902
+ let thenError = (events, expectedError) => {
903
+ expects(undefined, Primitive_option.some(expectedError));
904
+ return unlessRefused(events, __x => matchesError(__x, [], expectedError));
905
+ };
906
+ let thenEventWithError = (events, expectedEvent, expectedError) => {
907
+ expects([expectedEvent], Primitive_option.some(expectedError));
908
+ return unlessRefused(events, __x => matchesError(__x, [expectedEvent], expectedError));
909
+ };
910
+ let thenEventsWithError = (events, expectedEvents, expectedError) => {
911
+ expects(expectedEvents, Primitive_option.some(expectedError));
912
+ return unlessRefused(events, __x => matchesError(__x, expectedEvents, expectedError));
913
+ };
864
914
  let thenRefused = events => {
865
915
  let decided = Stdlib_Option.map(errors.contents[0], encError);
866
916
  let match = refusal.contents;
@@ -0,0 +1,182 @@
1
+ // Behaviour 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("Behavior")),
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: ""}])
@@ -0,0 +1,287 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
7
+
8
+ function enabled() {
9
+ let match = process.env["REVENTLESS_EMIT_SIDECAR"];
10
+ if (match === undefined) {
11
+ return false;
12
+ }
13
+ switch (match) {
14
+ case "1" :
15
+ case "TRUE" :
16
+ case "true" :
17
+ return true;
18
+ default:
19
+ return false;
20
+ }
21
+ }
22
+
23
+ let current = {
24
+ contents: undefined
25
+ };
26
+
27
+ let fileOverride = {
28
+ contents: undefined
29
+ };
30
+
31
+ function setFile(path) {
32
+ fileOverride.contents = path;
33
+ }
34
+
35
+ let jestTestPath = (function () {
36
+ try {
37
+ const state = globalThis.expect && globalThis.expect.getState && globalThis.expect.getState();
38
+ return state && typeof state.testPath === "string" ? state.testPath : undefined;
39
+ } catch (_) { return undefined; }
40
+ });
41
+
42
+ function testFile() {
43
+ let f = fileOverride.contents;
44
+ if (f !== undefined) {
45
+ return f;
46
+ } else {
47
+ return jestTestPath();
48
+ }
49
+ }
50
+
51
+ let recorded = new Map();
52
+
53
+ let specsOf = new Map();
54
+
55
+ let foreign = new Set();
56
+
57
+ function baseOf(file) {
58
+ return Stdlib_Array.reduce([
59
+ ".res.mjs",
60
+ ".res.js",
61
+ ".res",
62
+ ".mjs",
63
+ ".js"
64
+ ], file, (f, ext) => {
65
+ if (f.endsWith(ext)) {
66
+ return f.slice(0, f.length - ext.length | 0);
67
+ } else {
68
+ return f;
69
+ }
70
+ });
71
+ }
72
+
73
+ function sidecarFor(file, spec) {
74
+ let specs = Stdlib_Option.getOr(specsOf.get(file), []);
75
+ if (!specs.includes(spec)) {
76
+ specs.push(spec);
77
+ specsOf.set(file, specs);
78
+ }
79
+ if (specs.indexOf(spec) === 0) {
80
+ return baseOf(file) + `.gwt.json`;
81
+ } else {
82
+ return baseOf(file) + `.` + spec + `.gwt.json`;
83
+ }
84
+ }
85
+
86
+ function writtenByPpx(path) {
87
+ if (!Nodefs.existsSync(path)) {
88
+ return false;
89
+ }
90
+ let d;
91
+ try {
92
+ d = JSON.parse(Nodefs.readFileSync(path, "utf8"));
93
+ } catch (exn) {
94
+ return true;
95
+ }
96
+ if (typeof d === "object" && d !== null && !Array.isArray(d)) {
97
+ return Primitive_object.notequal(d["recorded"], true);
98
+ } else {
99
+ return true;
100
+ }
101
+ }
102
+
103
+ function entryJson(e) {
104
+ return Object.fromEntries([
105
+ [
106
+ "kind",
107
+ e.kind
108
+ ],
109
+ [
110
+ "element",
111
+ e.element
112
+ ],
113
+ [
114
+ "values",
115
+ []
116
+ ]
117
+ ]);
118
+ }
119
+
120
+ function write(path, file, spec, scenarios) {
121
+ let json = Object.fromEntries([
122
+ [
123
+ "specName",
124
+ spec + ` (recorded)`
125
+ ],
126
+ [
127
+ "stem",
128
+ spec
129
+ ],
130
+ [
131
+ "file",
132
+ file
133
+ ],
134
+ [
135
+ "componentKind",
136
+ "Behavior"
137
+ ],
138
+ [
139
+ "recorded",
140
+ true
141
+ ],
142
+ [
143
+ "scenarios",
144
+ scenarios.map(s => (Object.fromEntries([
145
+ [
146
+ "title",
147
+ s.title
148
+ ],
149
+ [
150
+ "given",
151
+ s.given.map(entryJson)
152
+ ],
153
+ [
154
+ "when",
155
+ s.when_.map(entryJson)
156
+ ],
157
+ [
158
+ "then",
159
+ s.then_.map(entryJson)
160
+ ]
161
+ ])))
162
+ ]
163
+ ]);
164
+ Nodefs.writeFileSync(path, Stdlib_Option.getOr(JSON.stringify(json, undefined, 2), "{}") + "\n", "utf8");
165
+ }
166
+
167
+ function around(spec, title, body) {
168
+ if (!enabled()) {
169
+ return body();
170
+ }
171
+ current.contents = {
172
+ title: title,
173
+ given: [],
174
+ when_: [],
175
+ then_: []
176
+ };
177
+ let finish = () => {
178
+ let scenario = current.contents;
179
+ current.contents = undefined;
180
+ let match = testFile();
181
+ if (scenario === undefined) {
182
+ return;
183
+ }
184
+ if (match === undefined) {
185
+ return;
186
+ }
187
+ let path = sidecarFor(match, spec);
188
+ if (!foreign.has(path) && !recorded.has(path) && writtenByPpx(path)) {
189
+ foreign.add(path);
190
+ }
191
+ if (foreign.has(path)) {
192
+ return;
193
+ }
194
+ let match$1 = Stdlib_Option.getOr(recorded.get(path), [
195
+ spec,
196
+ []
197
+ ]);
198
+ let scenarios = match$1[1].filter(o => o.title !== scenario.title).concat([scenario]);
199
+ recorded.set(path, [
200
+ spec,
201
+ scenarios
202
+ ]);
203
+ write(path, match, spec, scenarios);
204
+ };
205
+ let outcome;
206
+ try {
207
+ outcome = body();
208
+ } catch (e) {
209
+ finish();
210
+ throw e;
211
+ }
212
+ finish();
213
+ return outcome;
214
+ }
215
+
216
+ function recording() {
217
+ return Stdlib_Option.isSome(current.contents);
218
+ }
219
+
220
+ function push(pick, entries) {
221
+ Stdlib_Option.forEach(current.contents, s => {
222
+ entries.forEach(e => {
223
+ pick(s).push(e);
224
+ });
225
+ });
226
+ }
227
+
228
+ function given(names) {
229
+ push(s => s.given, names.map(element => ({
230
+ kind: "event",
231
+ element: element
232
+ })));
233
+ }
234
+
235
+ function whenCommand(name) {
236
+ push(s => s.when_, [{
237
+ kind: "command",
238
+ element: name
239
+ }]);
240
+ }
241
+
242
+ function thenEvents(names) {
243
+ push(s => s.then_, names.map(element => ({
244
+ kind: "event",
245
+ element: element
246
+ })));
247
+ }
248
+
249
+ function thenError(name) {
250
+ push(s => s.then_, [{
251
+ kind: "error",
252
+ element: name
253
+ }]);
254
+ }
255
+
256
+ function thenNoEvent() {
257
+ push(s => s.then_, [{
258
+ kind: "noEvent",
259
+ element: ""
260
+ }]);
261
+ }
262
+
263
+ export {
264
+ enabled,
265
+ current,
266
+ fileOverride,
267
+ setFile,
268
+ jestTestPath,
269
+ testFile,
270
+ recorded,
271
+ specsOf,
272
+ foreign,
273
+ baseOf,
274
+ sidecarFor,
275
+ writtenByPpx,
276
+ entryJson,
277
+ write,
278
+ around,
279
+ recording,
280
+ push,
281
+ given,
282
+ whenCommand,
283
+ thenEvents,
284
+ thenError,
285
+ thenNoEvent,
286
+ }
287
+ /* recorded Not a pure module */
@@ -14,7 +14,6 @@ module CancelUnshipped = {
14
14
  type command = CancelOrder({orderId: string})
15
15
 
16
16
  let maxRetries = 3
17
- let heartbeatInterval = 60
18
17
  let targetName = "Order"
19
18
  let schedule = None
20
19
  }
@@ -31,7 +31,6 @@ let CancelUnshipped = {
31
31
  todoItemSchema: todoItemSchema,
32
32
  commandSchema: commandSchema,
33
33
  maxRetries: 3,
34
- heartbeatInterval: 60,
35
34
  targetName: "Order",
36
35
  schedule: undefined,
37
36
  authorizationOf: authorizationOf,
@@ -16,7 +16,6 @@ module ShipPaid = {
16
16
  | GiveUp({orderId: string})
17
17
 
18
18
  let maxRetries = 3
19
- let heartbeatInterval = 60
20
19
  let targetName = "Order"
21
20
  let schedule = None
22
21
  }
@@ -36,7 +36,6 @@ let ShipPaid = {
36
36
  todoItemSchema: todoItemSchema,
37
37
  commandSchema: commandSchema,
38
38
  maxRetries: 3,
39
- heartbeatInterval: 60,
40
39
  targetName: "Order",
41
40
  schedule: undefined,
42
41
  authorizationOf: authorizationOf,
@@ -14,7 +14,6 @@ module MarketingDigest = {
14
14
  type command = RequestNotification({recipientId: string, reference: string})
15
15
 
16
16
  let maxRetries = 3
17
- let heartbeatInterval = 60
18
17
  let targetName = "NotificationPreferences"
19
18
  let schedule = None
20
19
  }
@@ -164,7 +163,6 @@ module Reminder = {
164
163
  type command = RemindOrder({orderId: string})
165
164
 
166
165
  let maxRetries = 3
167
- let heartbeatInterval = 60
168
166
  let targetName = "Order"
169
167
  let schedule = None
170
168
  }
@@ -34,7 +34,6 @@ let MarketingDigest = {
34
34
  todoItemSchema: todoItemSchema,
35
35
  commandSchema: commandSchema,
36
36
  maxRetries: 3,
37
- heartbeatInterval: 60,
38
37
  targetName: "NotificationPreferences",
39
38
  schedule: undefined,
40
39
  authorizationOf: authorizationOf,
@@ -419,7 +418,6 @@ let Reminder = {
419
418
  todoItemSchema: todoItemSchema$1,
420
419
  commandSchema: commandSchema$1,
421
420
  maxRetries: 3,
422
- heartbeatInterval: 60,
423
421
  targetName: "Order",
424
422
  schedule: undefined,
425
423
  authorizationOf: authorizationOf$1,
@@ -14,7 +14,6 @@ module DemandReport = {
14
14
  type command = RequestDemandReport({day: float})
15
15
 
16
16
  let maxRetries = 3
17
- let heartbeatInterval = 60
18
17
  let targetName = "ReportSettings"
19
18
  let schedule = Some(Reventless.Schedule.Calendar({calendar: Daily({hour: 6, minute: 0})}))
20
19
  }
@@ -41,7 +41,6 @@ let DemandReport = {
41
41
  todoItemSchema: todoItemSchema,
42
42
  commandSchema: commandSchema,
43
43
  maxRetries: 3,
44
- heartbeatInterval: 60,
45
44
  targetName: "ReportSettings",
46
45
  schedule: DemandReport_schedule,
47
46
  authorizationOf: authorizationOf,
@@ -15,7 +15,6 @@ module Welcome = {
15
15
  type inboundCommand = MarkWelcomed({customerId: string}) | MarkUnreachable({customerId: string})
16
16
 
17
17
  let maxRetries = 2
18
- let heartbeatInterval = 60
19
18
  let targetName = Some("Customer")
20
19
  let sourceNames = []
21
20
  let externalSystem = None
@@ -42,7 +42,6 @@ let Welcome = {
42
42
  outboundItemSchema: outboundItemSchema,
43
43
  inboundCommandSchema: inboundCommandSchema,
44
44
  maxRetries: 2,
45
- heartbeatInterval: 60,
46
45
  targetName: Welcome_targetName,
47
46
  sourceNames: sourceNames,
48
47
  externalSystem: undefined,
@@ -0,0 +1,101 @@
1
+ open JestGlobals
2
+
3
+ // `ScenarioRecorder` writes what a behaviour test runs as a sidecar `check-lifecycle` reads, for
4
+ // a suite whose source names nothing (one written against a binding). Each case records into a
5
+ // file of its own under the OS temp folder, named by `setFile` as an external runner would.
6
+
7
+ let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "scenario-recorder-"]))
8
+ let testFile = name => NodePath.join([dir, `${name}_GWT.res.mjs`])
9
+ let sidecarOf = name => NodePath.join([dir, `${name}_GWT.gwt.json`])
10
+
11
+ let read = path => NodeFs.readFileSync(path)->JSON.parseOrThrow
12
+
13
+ let field = (json: JSON.t, key) =>
14
+ switch json {
15
+ | Object(d) => d->Dict.get(key)
16
+ | _ => None
17
+ }
18
+
19
+ // One recorded test: the history, the command, and the event it expects.
20
+ let run = (~spec, ~title, ~expects) =>
21
+ ScenarioRecorder.around(~spec, title, () => {
22
+ ScenarioRecorder.given(["CustomerRegistered"])
23
+ ScenarioRecorder.whenCommand("RequestNotification")
24
+ ScenarioRecorder.thenEvents([expects])
25
+ })
26
+
27
+ let switched = (on, f) => {
28
+ NodeProcess.env->Dict.set("REVENTLESS_EMIT_SIDECAR", on ? "1" : "0")
29
+ let out = f()
30
+ NodeProcess.env->Dict.delete("REVENTLESS_EMIT_SIDECAR")
31
+ out
32
+ }
33
+
34
+ describe("ScenarioRecorder", () => {
35
+ testSync("with the switch off it writes nothing", () => {
36
+ ScenarioRecorder.setFile(testFile("Off"))
37
+ switched(false, () => run(~spec="Prefs", ~title="t", ~expects="NotificationRequested"))
38
+ expect(NodeFs.existsSync(sidecarOf("Off")))->toBe(false)
39
+ })
40
+
41
+ testSync("a test run with the switch on is a sidecar naming its spec as the component", () => {
42
+ ScenarioRecorder.setFile(testFile("Conformance"))
43
+ switched(
44
+ true,
45
+ () => {
46
+ run(~spec="Prefs", ~title="undeliverable", ~expects="NotificationUndeliverable")
47
+ run(~spec="Prefs", ~title="deferred", ~expects="NotificationDeferred")
48
+ },
49
+ )
50
+ let json = read(sidecarOf("Conformance"))
51
+ let scenarios = switch json->field("scenarios") {
52
+ | Some(Array(xs)) => xs
53
+ | _ => []
54
+ }
55
+ expect((
56
+ json->field("stem"),
57
+ json->field("recorded"),
58
+ scenarios->Array.map(
59
+ s =>
60
+ switch (s->field("when"), s->field("then")) {
61
+ | (Some(Array([w])), Some(Array([t]))) => (w->field("element"), t->field("element"))
62
+ | _ => (None, None)
63
+ },
64
+ ),
65
+ ))->toEqual((
66
+ Some(JSON.String("Prefs")),
67
+ Some(JSON.Boolean(true)),
68
+ [
69
+ (Some(JSON.String("RequestNotification")), Some(JSON.String("NotificationUndeliverable"))),
70
+ (Some(JSON.String("RequestNotification")), Some(JSON.String("NotificationDeferred"))),
71
+ ],
72
+ ))
73
+ })
74
+
75
+ testSync("a sidecar the PPX wrote is left as it is", () => {
76
+ let ppx = `{"stem":"Mine_GWT","scenarios":[]}`
77
+ NodeFs.writeFileSync(sidecarOf("Mine"), ppx)
78
+ ScenarioRecorder.setFile(testFile("Mine"))
79
+ switched(true, () => run(~spec="Prefs", ~title="t", ~expects="NotificationRequested"))
80
+ expect(NodeFs.readFileSync(sidecarOf("Mine")))->toBe(ppx)
81
+ })
82
+
83
+ testSync("a second spec in one file gets a sidecar of its own", () => {
84
+ ScenarioRecorder.setFile(testFile("Two"))
85
+ switched(
86
+ true,
87
+ () => {
88
+ run(~spec="Prefs", ~title="a", ~expects="NotificationRequested")
89
+ run(~spec="Claims", ~title="b", ~expects="NotificationSourceClaimed")
90
+ },
91
+ )
92
+ expect((
93
+ read(sidecarOf("Two"))->field("stem"),
94
+ read(NodePath.join([dir, "Two_GWT.Claims.gwt.json"]))->field("stem"),
95
+ ))->toEqual((Some(JSON.String("Prefs")), Some(JSON.String("Claims"))))
96
+ })
97
+
98
+ testSync("steps outside a recorded test record nothing", () =>
99
+ expect(ScenarioRecorder.recording())->toBe(false)
100
+ )
101
+ })
@@ -0,0 +1,163 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodeos from "node:os";
5
+ import * as Nodepath from "node:path";
6
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
7
+ import * as ScenarioRecorder$ReventlessGwt from "../src/ScenarioRecorder.res.mjs";
8
+
9
+ let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "scenario-recorder-"));
10
+
11
+ function testFile(name) {
12
+ return Nodepath.join(dir, name + `_GWT.res.mjs`);
13
+ }
14
+
15
+ function sidecarOf(name) {
16
+ return Nodepath.join(dir, name + `_GWT.gwt.json`);
17
+ }
18
+
19
+ function read(path) {
20
+ return JSON.parse(Nodefs.readFileSync(path, "utf8"));
21
+ }
22
+
23
+ function field(json, key) {
24
+ if (typeof json === "object" && json !== null && !Array.isArray(json)) {
25
+ return json[key];
26
+ }
27
+ }
28
+
29
+ function run(spec, title, expects) {
30
+ ScenarioRecorder$ReventlessGwt.around(spec, title, () => {
31
+ ScenarioRecorder$ReventlessGwt.given(["CustomerRegistered"]);
32
+ ScenarioRecorder$ReventlessGwt.whenCommand("RequestNotification");
33
+ ScenarioRecorder$ReventlessGwt.thenEvents([expects]);
34
+ });
35
+ }
36
+
37
+ function switched(on, f) {
38
+ process.env["REVENTLESS_EMIT_SIDECAR"] = on ? "1" : "0";
39
+ let out = f();
40
+ Stdlib_Dict.$$delete(process.env, "REVENTLESS_EMIT_SIDECAR");
41
+ return out;
42
+ }
43
+
44
+ globalThis.describe("ScenarioRecorder", () => {
45
+ globalThis.test("with the switch off it writes nothing", () => {
46
+ ScenarioRecorder$ReventlessGwt.setFile(testFile("Off"));
47
+ switched(false, () => run("Prefs", "t", "NotificationRequested"));
48
+ globalThis.expect(Nodefs.existsSync(sidecarOf("Off"))).toBe(false);
49
+ });
50
+ globalThis.test("a test run with the switch on is a sidecar naming its spec as the component", () => {
51
+ ScenarioRecorder$ReventlessGwt.setFile(testFile("Conformance"));
52
+ switched(true, () => {
53
+ run("Prefs", "undeliverable", "NotificationUndeliverable");
54
+ run("Prefs", "deferred", "NotificationDeferred");
55
+ });
56
+ let path = sidecarOf("Conformance");
57
+ let json = JSON.parse(Nodefs.readFileSync(path, "utf8"));
58
+ let match = field(json, "scenarios");
59
+ let scenarios = match !== undefined ? (
60
+ Array.isArray(match) ? match : []
61
+ ) : [];
62
+ globalThis.expect([
63
+ field(json, "stem"),
64
+ field(json, "recorded"),
65
+ scenarios.map(s => {
66
+ let match = field(s, "when");
67
+ let match$1 = field(s, "then");
68
+ if (match === undefined) {
69
+ return [
70
+ undefined,
71
+ undefined
72
+ ];
73
+ }
74
+ if (!Array.isArray(match)) {
75
+ return [
76
+ undefined,
77
+ undefined
78
+ ];
79
+ }
80
+ if (match.length !== 1) {
81
+ return [
82
+ undefined,
83
+ undefined
84
+ ];
85
+ }
86
+ let w = match[0];
87
+ if (match$1 === undefined) {
88
+ return [
89
+ undefined,
90
+ undefined
91
+ ];
92
+ }
93
+ if (!Array.isArray(match$1)) {
94
+ return [
95
+ undefined,
96
+ undefined
97
+ ];
98
+ }
99
+ if (match$1.length !== 1) {
100
+ return [
101
+ undefined,
102
+ undefined
103
+ ];
104
+ }
105
+ let t = match$1[0];
106
+ return [
107
+ field(w, "element"),
108
+ field(t, "element")
109
+ ];
110
+ })
111
+ ]).toEqual([
112
+ "Prefs",
113
+ true,
114
+ [
115
+ [
116
+ "RequestNotification",
117
+ "NotificationUndeliverable"
118
+ ],
119
+ [
120
+ "RequestNotification",
121
+ "NotificationDeferred"
122
+ ]
123
+ ]
124
+ ]);
125
+ });
126
+ globalThis.test("a sidecar the PPX wrote is left as it is", () => {
127
+ let ppx = `{"stem":"Mine_GWT","scenarios":[]}`;
128
+ Nodefs.writeFileSync(sidecarOf("Mine"), ppx, "utf8");
129
+ ScenarioRecorder$ReventlessGwt.setFile(testFile("Mine"));
130
+ switched(true, () => run("Prefs", "t", "NotificationRequested"));
131
+ globalThis.expect(Nodefs.readFileSync(sidecarOf("Mine"), "utf8")).toBe(ppx);
132
+ });
133
+ globalThis.test("a second spec in one file gets a sidecar of its own", () => {
134
+ ScenarioRecorder$ReventlessGwt.setFile(testFile("Two"));
135
+ switched(true, () => {
136
+ run("Prefs", "a", "NotificationRequested");
137
+ run("Claims", "b", "NotificationSourceClaimed");
138
+ });
139
+ let path = sidecarOf("Two");
140
+ let path$1 = Nodepath.join(dir, "Two_GWT.Claims.gwt.json");
141
+ globalThis.expect([
142
+ field(JSON.parse(Nodefs.readFileSync(path, "utf8")), "stem"),
143
+ field(JSON.parse(Nodefs.readFileSync(path$1, "utf8")), "stem")
144
+ ]).toEqual([
145
+ "Prefs",
146
+ "Claims"
147
+ ]);
148
+ });
149
+ globalThis.test("steps outside a recorded test record nothing", () => {
150
+ globalThis.expect(ScenarioRecorder$ReventlessGwt.recording()).toBe(false);
151
+ });
152
+ });
153
+
154
+ export {
155
+ dir,
156
+ testFile,
157
+ sidecarOf,
158
+ read,
159
+ field,
160
+ run,
161
+ switched,
162
+ }
163
+ /* dir Not a pure module */