@reventlessdev/reventless-gwt 1.0.0-alpha.247 → 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 +7 -0
- package/package.json +5 -5
- package/src/Behavior_GWT.res +85 -15
- package/src/Behavior_GWT.res.mjs +82 -32
- package/src/ScenarioRecorder.res +182 -0
- package/src/ScenarioRecorder.res.mjs +287 -0
- package/tests/ScenarioRecorderTest.res +101 -0
- package/tests/ScenarioRecorderTest.res.mjs +163 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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
|
+
|
|
6
13
|
# 1.0.0-alpha.247 (2026-10-17)
|
|
7
14
|
|
|
8
15
|
**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.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,11 +15,11 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"sury": "11.0.0",
|
|
17
17
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
18
|
-
"@reventlessdev/
|
|
19
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.299",
|
|
20
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.196",
|
|
18
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.300",
|
|
21
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
22
|
-
"@reventlessdev/reventless-
|
|
20
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.197",
|
|
21
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
22
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.169"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"rescript": "12.3.1",
|
package/src/Behavior_GWT.res
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
823
|
-
|
|
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 =>
|
|
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(
|
package/src/Behavior_GWT.res.mjs
CHANGED
|
@@ -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) =>
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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 =>
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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 */
|
|
@@ -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 */
|