@peter.naydenov/cuts 2.1.2 → 2.1.4

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.
@@ -455,7 +455,7 @@ describe ( 'Cuts integration', () => {
455
455
  await script.show ({ scene: 'overlay' });
456
456
  expect ( calls ).toEqual (['show overlay']);
457
457
  expect ( script.getState().scene ).toEqual ( 'overlay' );
458
- expect ( script.getState().parents ).toEqual ( [null] ); // No previous scene, saved as null
458
+ expect ( script.getState().parents ).toEqual ( [] ); // No underlying scene to remember
459
459
  }) // it Show scene with wildcard parent as the very first scene
460
460
 
461
461
 
@@ -578,4 +578,353 @@ describe ( 'Cuts integration', () => {
578
578
 
579
579
 
580
580
 
581
+ it ( 'getState() does not leak a mutable reference to internal state', async () => {
582
+ // Regression test: getState() used to return state.currentParents by
583
+ // reference, so a caller mutating the returned array corrupted cuts' own state.
584
+ const script = cuts();
585
+ script.setScenes ([
586
+ { name: 'home', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done() } }
587
+ ]);
588
+
589
+ await script.show ({ scene: 'home' });
590
+ const firstRead = script.getState ();
591
+ firstRead.parents.push ( 'INJECTED' );
592
+
593
+ expect ( script.getState().parents ).toEqual ([]);
594
+ }) // it getState() does not leak a mutable reference to internal state
595
+
596
+
597
+
598
+ it ( 'hide() restores state and shortcuts context after dismissing a wildcard overlay', async () => {
599
+ // Regression test: hide() on a wildcard-overlay scene left currentScene/currentParents
600
+ // stale (still pointing at the now-hidden overlay), so the underlying scene's
601
+ // shortcuts context was never reactivated even though it was still visible.
602
+ const seen = [];
603
+ const script = cuts();
604
+ const scenes = [
605
+ {
606
+ name: 'base'
607
+ , scene: {
608
+ show : ({task}) => task.done(),
609
+ hide : ({task}) => task.done(),
610
+ '@ping' : () => seen.push ( 'base' )
611
+ }
612
+ },
613
+ {
614
+ name: 'overlay'
615
+ , scene: {
616
+ show : ({task}) => task.done(),
617
+ hide : ({task}) => task.done(),
618
+ parents: ['*'],
619
+ '@ping' : () => seen.push ( 'overlay' )
620
+ }
621
+ }
622
+ ];
623
+
624
+ script.setScenes ( scenes );
625
+ await script.show ({ scene: 'base' });
626
+ await script.show ({ scene: 'overlay' });
627
+
628
+ await script.hide (); // Dismiss the overlay
629
+ expect ( script.getState() ).toEqual ({ scene: 'base', parents: [], opened: true });
630
+
631
+ script.emit ( '@ping' );
632
+ expect ( seen ).toEqual ([ 'base' ]);
633
+ }) // it hide() restores state and shortcuts context after dismissing a wildcard overlay
634
+
635
+
636
+
637
+ it ( 'logLevel: 0 is honored as explicitly silent, and the no-args default stays 1', async () => {
638
+ // Regression test: 'cfg.logLevel || 1' treated an explicit 0 as falsy and
639
+ // silently forced logLevel back to 1, so 'silent mode' never actually worked.
640
+ const silentSeen = [];
641
+ const silentScript = cuts ({ logLevel: 0 });
642
+ silentScript.setScenes ([
643
+ { name: 'top', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done(), '@app-error': (e) => silentSeen.push(e) } }
644
+ ]);
645
+ await silentScript.show ({ scene: 'top' });
646
+ await silentScript.show ({ scene: 'nope' });
647
+ expect ( silentSeen ).toEqual ([]);
648
+
649
+ const defaultSeen = [];
650
+ const defaultScript = cuts (); // No config at all - README default is logLevel 1
651
+ defaultScript.setScenes ([
652
+ { name: 'top', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done(), '@app-error': (e) => defaultSeen.push(e) } }
653
+ ]);
654
+ await defaultScript.show ({ scene: 'top' });
655
+ await defaultScript.show ({ scene: 'nope' });
656
+ expect ( defaultSeen ).toHaveLength ( 1 );
657
+ }) // it logLevel: 0 is honored as explicitly silent, and the no-args default stays 1
658
+
659
+
660
+
661
+ it ( 'hide() with the default single step climbs to the parent and restores its shortcuts context', async () => {
662
+ // Regression test: hide() used to leave currentScene/currentParents pointing at
663
+ // the just-hidden scene instead of climbing to its (still visible) parent, so
664
+ // NEITHER scene's shortcuts context was active afterward.
665
+ const seen = [];
666
+ const script = cuts();
667
+ const scenes = [
668
+ {
669
+ name: 'A'
670
+ , scene: {
671
+ show : ({task}) => task.done(),
672
+ hide : ({task}) => task.done(),
673
+ '@ping' : () => seen.push ( 'A' )
674
+ }
675
+ },
676
+ {
677
+ name: 'B'
678
+ , scene: {
679
+ show : ({task}) => task.done(),
680
+ hide : ({task}) => task.done(),
681
+ parents: ['A'],
682
+ '@ping' : () => seen.push ( 'B' )
683
+ }
684
+ }
685
+ ];
686
+
687
+ script.setScenes ( scenes );
688
+ await script.show ({ scene: 'A' });
689
+ await script.show ({ scene: 'B' });
690
+
691
+ await script.hide (); // default endSteps = 1
692
+ expect ( script.getState() ).toEqual ({ scene: 'A', parents: [], opened: true });
693
+
694
+ script.emit ( '@ping' );
695
+ expect ( seen ).toEqual ([ 'A' ]);
696
+ }) // it hide() with the default single step climbs to the parent and restores its shortcuts context
697
+
698
+
699
+
700
+ it ( 'hide(n) climbs exactly n levels', async () => {
701
+ const calls = [];
702
+ const script = cuts();
703
+ const scenes = [
704
+ { name: 'top' , scene: { show: ({task}) => { calls.push('show top'); task.done() }, hide: ({task}) => { calls.push('hide top'); task.done() } } },
705
+ { name: 'mid' , scene: { show: ({task}) => { calls.push('show mid'); task.done() }, hide: ({task}) => { calls.push('hide mid'); task.done() }, parents: ['top'] } },
706
+ { name: 'deep', scene: { show: ({task}) => { calls.push('show deep'); task.done() }, hide: ({task}) => { calls.push('hide deep'); task.done() }, parents: ['top', 'mid'] } }
707
+ ];
708
+
709
+ script.setScenes ( scenes );
710
+ await script.show ({ scene: 'deep' });
711
+ calls.length = 0;
712
+
713
+ await script.hide ( 2 );
714
+ expect ( calls ).toEqual ([ 'hide deep', 'hide mid' ]);
715
+ expect ( script.getState() ).toEqual ({ scene: 'top', parents: [], opened: true });
716
+ }) // it hide(n) climbs exactly n levels
717
+
718
+
719
+
720
+ it ( 'SSR first load records the scene\'s parents, so leaving it hides them too', async () => {
721
+ // Regression test: the SSR branch of show() set currentScene but never
722
+ // currentParents, so navigating away from a deep SSR-loaded scene skipped
723
+ // hiding its ancestors entirely, leaving them stale in the DOM.
724
+ const calls = [];
725
+ const script = cuts();
726
+ const scenes = [
727
+ { name: 'top' , scene: { show: ({task}) => { calls.push('show top'); task.done() }, hide: ({task}) => { calls.push('hide top'); task.done() } } },
728
+ { name: 'mid' , scene: { show: ({task}) => { calls.push('show mid'); task.done() }, hide: ({task}) => { calls.push('hide mid'); task.done() }, parents: ['top'] } },
729
+ { name: 'deep', scene: { show: ({task}) => { calls.push('show deep'); task.done() }, hide: ({task}) => { calls.push('hide deep'); task.done() }, parents: ['top', 'mid'] } },
730
+ { name: 'other', scene: { show: ({task}) => { calls.push('show other'); task.done() }, hide: ({task}) => { calls.push('hide other'); task.done() } } }
731
+ ];
732
+
733
+ script.setScenes ( scenes );
734
+ await script.show ({ scene: 'deep', options: { ssr: true } });
735
+ expect ( script.getState() ).toEqual ({ scene: 'deep', parents: [ 'top', 'mid' ], opened: true });
736
+
737
+ calls.length = 0;
738
+ await script.show ({ scene: 'other' });
739
+ expect ( calls ).toEqual ([ 'hide deep', 'hide mid', 'hide top', 'show other' ]);
740
+ }) // it SSR first load records the scene's parents, so leaving it hides them too
741
+
742
+
743
+
744
+ it ( 'afterShow return value has no effect on the completed show()', async () => {
745
+ // Regression test: setScenes.js's typedef documented afterShow as
746
+ // 'returns false to cancel', but show() never read its return value, so
747
+ // the scene was always shown regardless. The typedef is now corrected to
748
+ // describe the actual (README-documented) fire-and-forget behavior.
749
+ const calls = [];
750
+ const script = cuts();
751
+ const scenes = [
752
+ {
753
+ name: 'top'
754
+ , scene: {
755
+ show : ({task}) => { calls.push('show'); task.done() },
756
+ hide : ({task}) => task.done(),
757
+ afterShow : () => { calls.push('afterShow'); return false }
758
+ }
759
+ }
760
+ ];
761
+
762
+ script.setScenes ( scenes );
763
+ await script.show ({ scene: 'top' });
764
+
765
+ expect ( calls ).toEqual ([ 'show', 'afterShow' ]);
766
+ expect ( script.getState() ).toEqual ({ scene: 'top', parents: [], opened: true });
767
+ }) // it afterShow return value has no effect on the completed show()
768
+
769
+
770
+
771
+ it ( 'hide() honors beforeHide, same as show() does', async () => {
772
+ // Regression test: hide() used to ignore beforeHide entirely, so it could
773
+ // never be blocked - even though show() already respected it when navigating
774
+ // away to a different scene.
775
+ const calls = [];
776
+ const script = cuts();
777
+ const scenes = [
778
+ {
779
+ name: 'top'
780
+ , scene: {
781
+ show : ({task}) => { calls.push('show top'); task.done() },
782
+ hide : ({task}) => { calls.push('hide top'); task.done() },
783
+ beforeHide : ({ done }) => { calls.push('beforeHide'); done ( false ) }
784
+ }
785
+ }
786
+ ];
787
+
788
+ script.setScenes ( scenes );
789
+ await script.show ({ scene: 'top' });
790
+ calls.length = 0;
791
+
792
+ await script.hide ();
793
+ expect ( calls ).toEqual ([ 'beforeHide' ]); // hide() never actually ran
794
+ expect ( script.getState() ).toEqual ({ scene: 'top', parents: [], opened: true });
795
+ }) // it hide() honors beforeHide, same as show() does
796
+
797
+
798
+
799
+ it ( 'jump() does not record a jump-stack entry when the navigation is blocked', async () => {
800
+ // Regression test: jump() pushed onto the jump stack unconditionally, even when
801
+ // the underlying show() was blocked (eg. by beforeHide) and nothing actually
802
+ // changed - leaving a phantom entry that desynced jumpBack() from real history.
803
+ const calls = [];
804
+ const script = cuts();
805
+ let block = true;
806
+ const scenes = [
807
+ {
808
+ name: 'home'
809
+ , scene: {
810
+ show : ({task}) => { calls.push('show home'); task.done() },
811
+ hide : ({task}) => { calls.push('hide home'); task.done() },
812
+ beforeHide : ({ done }) => done ( !block )
813
+ }
814
+ },
815
+ {
816
+ name: 'settings'
817
+ , scene: {
818
+ show : ({task}) => { calls.push('show settings'); task.done() },
819
+ hide : ({task}) => { calls.push('hide settings'); task.done() }
820
+ }
821
+ },
822
+ {
823
+ name: 'other'
824
+ , scene: {
825
+ show : ({task}) => { calls.push('show other'); task.done() },
826
+ hide : ({task}) => { calls.push('hide other'); task.done() }
827
+ }
828
+ }
829
+ ];
830
+
831
+ script.setScenes ( scenes );
832
+ await script.show ({ scene: 'home' });
833
+
834
+ await script.jump ({ scene: 'settings' }); // blocked by beforeHide - no stack entry
835
+ expect ( script.getState().scene ).toEqual ( 'home' );
836
+
837
+ block = false;
838
+ await script.jump ({ scene: 'other' }); // succeeds - exactly one real stack entry
839
+ expect ( script.getState().scene ).toEqual ( 'other' );
840
+
841
+ calls.length = 0;
842
+ await script.jumpBack ();
843
+ expect ( calls ).toEqual ([ 'hide other', 'show home' ]);
844
+ expect ( script.getState().scene ).toEqual ( 'home' );
845
+
846
+ calls.length = 0;
847
+ await script.jumpBack (); // stack should already be empty - true no-op
848
+ expect ( calls ).toEqual ([]);
849
+ expect ( script.getState().scene ).toEqual ( 'home' );
850
+ }) // it jump() does not record a jump-stack entry when the navigation is blocked
851
+
852
+
853
+
854
+ it ( 'Navigating from a wildcard overlay to an unrelated scene hides the scene it was covering', async () => {
855
+ // Regression test: findInstructions is a lazy generator, and show() passed it
856
+ // state.currentParents by reference. getStep's wildcard climb (state.currentParents.pop())
857
+ // mutated that same array mid-iteration, corrupting the generator's still-pending reads
858
+ // and silently dropping the 'hide' instruction for the scene the overlay was covering -
859
+ // leaking it as permanently visible.
860
+ const calls = [];
861
+ const script = cuts();
862
+ const scenes = [
863
+ { name: 'G', scene: { show: ({task}) => { calls.push('show G'); task.done() }, hide: ({task}) => { calls.push('hide G'); task.done() } } },
864
+ { name: 'OV', scene: { show: ({task}) => { calls.push('show OV'); task.done() }, hide: ({task}) => { calls.push('hide OV'); task.done() }, parents: ['*'] } },
865
+ { name: 'D', scene: { show: ({task}) => { calls.push('show D'); task.done() }, hide: ({task}) => { calls.push('hide D'); task.done() } } }
866
+ ];
867
+
868
+ script.setScenes ( scenes );
869
+ await script.show ({ scene: 'G' });
870
+ await script.show ({ scene: 'OV' });
871
+ calls.length = 0;
872
+
873
+ await script.show ({ scene: 'D' });
874
+ expect ( calls ).toEqual ([ 'hide OV', 'hide G', 'show D' ]);
875
+ expect ( script.getState() ).toEqual ({ scene: 'D', parents: [], opened: true });
876
+ }) // it Navigating from a wildcard overlay to an unrelated scene hides the scene it was covering
877
+
878
+
879
+
880
+ it ( 'Showing a wildcard overlay with no underlying scene does not crash later navigation', async () => {
881
+ // Regression test: showing an overlay when state.currentScene was null recorded a
882
+ // literal 'null' placeholder in currentParents. Navigating away from it later crashed,
883
+ // because findInstructions/getStep treated that placeholder as a real scene name to hide.
884
+ const script = cuts();
885
+ const scenes = [
886
+ { name: 'F', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done() } },
887
+ { name: 'OV', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done(), parents: ['*'] } },
888
+ { name: 'A', scene: { show: ({task}) => task.done(), hide: ({task}) => task.done() } }
889
+ ];
890
+
891
+ script.setScenes ( scenes );
892
+ await script.show ({ scene: 'F' });
893
+ await script.hide ( 1 ); // back to nothing - currentScene becomes null
894
+ await script.show ({ scene: 'OV' }); // overlay with no underlying scene
895
+
896
+ expect ( script.getState() ).toEqual ({ scene: 'OV', parents: [], opened: true });
897
+
898
+ await script.show ({ scene: 'A' }); // used to throw
899
+ expect ( script.getState() ).toEqual ({ scene: 'A', parents: [], opened: true });
900
+ }) // it Showing a wildcard overlay with no underlying scene does not crash later navigation
901
+
902
+
903
+
904
+ it ( 'Re-showing the same wildcard overlay does not push it onto its own ancestor chain', async () => {
905
+ // Regression test: showing an overlay while it was ALREADY the current scene pushed
906
+ // it onto its own currentParents (self-reference). A later hide() would then pop that
907
+ // self-reference and report the overlay as still current, even though it had just been
908
+ // hidden - a state/reality desync.
909
+ const script = cuts();
910
+ const visible = new Set();
911
+ const scenes = [
912
+ { name: 'C', scene: { show: ({task}) => { visible.add('C'); task.done() }, hide: ({task}) => { visible.delete('C'); task.done() } } },
913
+ { name: 'OV', scene: { show: ({task}) => { visible.add('OV'); task.done() }, hide: ({task}) => { visible.delete('OV'); task.done() }, parents: ['*'] } }
914
+ ];
915
+
916
+ script.setScenes ( scenes );
917
+ await script.show ({ scene: 'C' });
918
+ await script.show ({ scene: 'OV' });
919
+ await script.show ({ scene: 'OV' }); // show the same overlay again while already current
920
+
921
+ expect ( script.getState() ).toEqual ({ scene: 'OV', parents: [ 'C' ], opened: true });
922
+
923
+ await script.hide ( 1 );
924
+ expect ( script.getState() ).toEqual ({ scene: 'C', parents: [], opened: true });
925
+ expect ( [...visible] ).toEqual ([ 'C' ]); // OV must actually be gone, matching reported state
926
+ }) // it Re-showing the same wildcard overlay does not push it onto its own ancestor chain
927
+
928
+
929
+
581
930
  }) // describe
package/types/main.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- declare function main(cfg?: {
2
- logLevel: number;
3
- }): {
1
+ declare function main(cfg?: {}): {
4
2
  hide: (endSteps?: number | string) => Promise<any>;
5
3
  listShortcuts: (sceneName: string) => (Array<string> | null);
6
4
  setScenes: (list: import("./methods/setScenes.js").sceneDescription[]) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.js"],"names":[],"mappings":"AAsCA,iBAAS,IAAI,CAAG,GAAG;IAAG,QAAQ;CAAG;;;;;;;;;;;;;;;;;IA0CzB;;;;;;;;;;;;;;OAcG;2BAPQ,KAAK,+CAAc,KACjB,OAAO,CAAE,UAAU,CAAC;IAajC;;;;;OAKG;4BAFQ,GAAC;IAKZ;;;;OAIG;2BADU,GAAC;IAId;;;;OAIG;oBAFQ,MAAM;IAKjB;;;;OAIG;sBADU,KAAK,CAAE,MAAM,CAAC;IAI3B;;;;;;OAMG;+CAFQ,GAAC;IAKZ;;;;;OAKG;gCAFQ,MAAM;IAKjB;;;;;;;OAOG;oBAJU,MAAM;IAYlB;;;;;;OAMG;kBAHQ,MAAM,WACH,GAAC,EAAA;EAOvB;eAIc,IAAI"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.js"],"names":[],"mappings":"AAsCA,iBAAS,IAAI,CAAG,GAAG,KAAI;;;;;;;;;;;;;;;;;IA2Cf;;;;;;;;;;;;;;OAcG;2BAPQ,KAAK,+CAAc,KACjB,OAAO,CAAE,UAAU,CAAC;IAajC;;;;;OAKG;4BAFQ,GAAC;IAKZ;;;;OAIG;2BADU,GAAC;IAId;;;;OAIG;oBAFQ,MAAM;IAKjB;;;;OAIG;sBADU,KAAK,CAAE,MAAM,CAAC;IAI3B;;;;;;OAMG;+CAFQ,GAAC;IAKZ;;;;;OAKG;gCAFQ,MAAM;IAKjB;;;;;;;OAOG;oBAJU,MAAM;IAalB;;;;;;OAMG;kBAHQ,MAAM,WACH,GAAC,EAAA;EAOvB;eAIc,IAAI"}
@@ -1 +1 @@
1
- {"version":3,"file":"hide.d.ts","sourceRoot":"","sources":["../../src/methods/hide.js"],"names":[],"mappings":"AAIA,iBAAS,IAAI,CAAG,YAAY,KAAA,EAAE,KAAK,KAAA,eAIxB,MAAM,GAAC,MAAM,kBAsCtB;eAIa,IAAI"}
1
+ {"version":3,"file":"hide.d.ts","sourceRoot":"","sources":["../../src/methods/hide.js"],"names":[],"mappings":"AAIA,iBAAS,IAAI,CAAG,YAAY,KAAA,EAAE,KAAK,KAAA,eAIxB,MAAM,GAAC,MAAM,kBAmDtB;eAIa,IAAI"}
package/vitest.config.js CHANGED
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'
3
3
  export default defineConfig({
4
4
  test: {
5
5
  environment: 'node',
6
- exclude: ['test-e2e/**', 'node_modules/**'],
6
+ exclude: ['test-e2e/**', '**/node_modules/**'],
7
7
  coverage: {
8
8
  provider: 'v8',
9
9
  reporter: ['lcov', 'text-summary'],