@datarailsshared/dr_renderer 1.3.57 → 1.4.5

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.
@@ -8,6 +8,7 @@ import addInDynamicRanges from './mock/add-in-dynamic-ranges.json';
8
8
  import widgets from './mock/widgets.json';
9
9
  import initPivotTable from "../src/pivottable";
10
10
  import initDRPivotTable from "../src/dr_pivottable";
11
+ import valueFormatter from "../src/value.formatter";
11
12
  import { DrGaugeChart, GAUGE_OPTIONS_DEFAULT } from "../src/charts/dr_gauge_chart";
12
13
 
13
14
  const mockDrChartRender = jest.fn();
@@ -68,6 +69,8 @@ let _document = document;
68
69
  let Highcharts;
69
70
 
70
71
  describe('highcharts_renderer', () => {
72
+ let getAggregatorPercentageValueIfRequiredMock;
73
+
71
74
  beforeAll(() => {
72
75
  Highcharts = {
73
76
  charts: [{
@@ -92,6 +95,12 @@ describe('highcharts_renderer', () => {
92
95
 
93
96
  highchartsRenderer = getHighchartsRenderer($, _document, Highcharts, lodash.cloneDeep(DEFAULT_USER_COLORS), highchartsRenderer,
94
97
  DataFormatter, lodash, moment, true);
98
+
99
+ getAggregatorPercentageValueIfRequiredMock = jest.spyOn(valueFormatter, 'getAggregatorPercentageValueIfRequired').mockImplementation(() => null);
100
+ });
101
+
102
+ afterAll(() => {
103
+ getAggregatorPercentageValueIfRequiredMock.mockRestore();
95
104
  });
96
105
 
97
106
  describe('Function filterFloat', () => {
@@ -511,19 +520,574 @@ describe('highcharts_renderer', () => {
511
520
  });
512
521
 
513
522
  describe('function defaultDataLabelFormatter', () => {
523
+ let mockPivotData;
514
524
  let funcContext;
515
525
  let opts;
516
526
 
517
527
  beforeEach(() => {
518
528
  highchartsRenderer.enabledNewWidgetValueFormatting = false;
519
- funcContext = { y: '12345.678' };
520
- opts = {}
529
+ highchartsRenderer.delimer = ' , ';
530
+
531
+ funcContext = {
532
+ y: 12345.678,
533
+ series: {
534
+ name: 'TestSeries',
535
+ userOptions: {},
536
+ options: {}
537
+ },
538
+ point: {
539
+ name: 'TestPoint',
540
+ options: {}
541
+ }
542
+ };
543
+ opts = {};
544
+
545
+ mockPivotData = {
546
+ rowAttrs: ['row1'],
547
+ colAttrs: ['col1'],
548
+ getColKeys: jest.fn(() => [['col1'], ['col2']]),
549
+ getRowKeys: jest.fn(() => [['row1'], ['row2']]),
550
+ getAggregator: jest.fn(() => ({
551
+ value: () => 1000
552
+ }))
553
+ };
554
+
555
+ spyOn(highchartsRenderer, 'getSeriesNameInFormatterContext').and.returnValue('TestSeries');
556
+ spyOn(highchartsRenderer, 'getColsInFormatterContext').and.returnValue(['col1']);
557
+ spyOn(highchartsRenderer, 'getOthersName').and.returnValue('Others');
558
+ spyOn(highchartsRenderer, 'getDrOthersInAxisState').and.returnValue({});
559
+ spyOn(highchartsRenderer, 'transformRowsAndColsForBreakdown').and.returnValue({
560
+ rows: ['row1'],
561
+ cols: ['col1']
562
+ });
563
+ spyOn(highchartsRenderer, 'replaceDrOthersKeys');
564
+ spyOn(highchartsRenderer, 'selfStartsWith').and.returnValue(false);
565
+ spyOn(highchartsRenderer, 'isChartWithMultiValues').and.returnValue(true);
566
+
567
+ global.$ = {
568
+ pivotUtilities: {
569
+ getFormattedNumber: jest.fn(() => '1,234.56')
570
+ }
571
+ };
521
572
  });
522
573
 
523
- it('should return local string if there are no pivotData', () => {
524
- let fn = highchartsRenderer.defaultDataLabelFormatter(null, {})
525
- let result = fn.call(funcContext)
526
- expect(result).toBe('12,345.678');
574
+ describe('No pivotData is provided', () => {
575
+ it('should return formatted number as local string', () => {
576
+ let fn = highchartsRenderer.defaultDataLabelFormatter(null, {});
577
+ let result = fn.call(funcContext);
578
+
579
+ expect(result).toBe('12,345.678');
580
+ });
581
+
582
+ it('should handle unit sign removal when labelOptions are provided', () => {
583
+ const labelOptions = { useUnitAbbreviation: false };
584
+ opts = { chartOptions: { label: labelOptions } };
585
+
586
+ let fn = highchartsRenderer.defaultDataLabelFormatter(null, opts);
587
+ let result = fn.call(funcContext);
588
+
589
+ expect(result).toBe('12,345.678');
590
+ });
591
+ });
592
+
593
+ describe('pivotData is provided', () => {
594
+ beforeEach(() => {
595
+ funcContext.y = 1234.56;
596
+ });
597
+
598
+ it('should format value using pivot data aggregator when show_value is true', () => {
599
+ opts = { chartOptions: { label: { show_value: true } } };
600
+
601
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
602
+ let result = fn.call(funcContext);
603
+
604
+ expect(highchartsRenderer.getSeriesNameInFormatterContext).toHaveBeenCalledWith(funcContext);
605
+ expect(highchartsRenderer.getColsInFormatterContext).toHaveBeenCalledWith(funcContext);
606
+ expect(mockPivotData.getAggregator).toHaveBeenCalled();
607
+ expect(result).toBe('1,234.56');
608
+ });
609
+
610
+ it('should return empty string when show_value is false and not drill-down pie', () => {
611
+ opts = { chartOptions: { label: { show_value: false } } };
612
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
613
+ let result = fn.call(funcContext);
614
+
615
+ expect(result).toBe('');
616
+ });
617
+
618
+ it('should handle empty row attributes', () => {
619
+ mockPivotData.rowAttrs = [];
620
+ opts = { chartOptions: { label: { show_value: true } } };
621
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
622
+ let result = fn.call(funcContext);
623
+
624
+ expect(result).toBe('1,234.56');
625
+ });
626
+
627
+ it('should handle totalSeries className', () => {
628
+ funcContext.series.options.className = 'totalSeries';
629
+ opts = { chartOptions: { label: { show_value: true } } };
630
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
631
+ let result = fn.call(funcContext);
632
+
633
+ expect(result).toBe('1,234.56');
634
+ });
635
+
636
+ it('should handle trendSeries className', () => {
637
+ funcContext.series.options.className = 'trendSeries';
638
+ opts = { chartOptions: { label: { show_value: true } } };
639
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
640
+ let result = fn.call(funcContext);
641
+
642
+ expect(result).toBe('1,234.56');
643
+ });
644
+ });
645
+
646
+ describe('Drill-down pie chart scenarios', () => {
647
+ beforeEach(() => {
648
+ funcContext.y = 500;
649
+ });
650
+
651
+ it('should handle drill-down pie when series name starts with "Series " and chart does not have multi values', () => {
652
+ highchartsRenderer.selfStartsWith.and.returnValue(true);
653
+ highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('Series 1');
654
+ highchartsRenderer.isChartWithMultiValues.and.returnValue(false);
655
+
656
+ opts = { chartOptions: {} };
657
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
658
+ let result = fn.call(funcContext);
659
+
660
+ expect(highchartsRenderer.selfStartsWith).toHaveBeenCalledWith('Series 1', 'Series ');
661
+ expect(highchartsRenderer.isChartWithMultiValues).toHaveBeenCalledWith(mockPivotData);
662
+ expect(result).toBe('500');
663
+ });
664
+
665
+ it('should handle drill-down pie when is a multi values chart', () => {
666
+ highchartsRenderer.isChartWithMultiValues.and.returnValue(true);
667
+
668
+ opts = { chartOptions: {} };
669
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
670
+ let result = fn.call(funcContext);
671
+
672
+ expect(highchartsRenderer.isChartWithMultiValues).toHaveBeenCalledWith(mockPivotData);
673
+ expect(result).toBe('500');
674
+ });
675
+
676
+ it('should use point name for columns when cols is null in drill-down pie', () => {
677
+ highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
678
+ funcContext.point.name = 'DrillDownPoint';
679
+
680
+ opts = { chartOptions: {} };
681
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
682
+ let result = fn.call(funcContext);
683
+
684
+ expect(result).toBe('500');
685
+ });
686
+
687
+ it('should swap rows and cols for drill-down pie when series name does not start with "Series "', () => {
688
+ highchartsRenderer.selfStartsWith.and.returnValue(false);
689
+ highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('CustomSeries');
690
+
691
+ opts = { chartOptions: {} };
692
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
693
+ let result = fn.call(funcContext);
694
+
695
+ expect(result).toBe('500');
696
+ });
697
+ });
698
+
699
+ describe('Delta column handling', () => {
700
+ it('should replace variant name when delta column field is series', () => {
701
+ opts = {
702
+ chartOptions: {
703
+ label: { show_value: true },
704
+ delta_column: {
705
+ field: 'series',
706
+ name: 'test_variant'
707
+ }
708
+ }
709
+ };
710
+
711
+ highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('test_variant' + highchartsRenderer.delimer + 'other');
712
+
713
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
714
+ let result = fn.call(funcContext);
715
+
716
+ expect(result).toBe('12,345.678');
717
+ });
718
+
719
+ it('should handle variant name matching when series name differs by underscores from delta column name', () => {
720
+ opts = {
721
+ chartOptions: {
722
+ label: { show_value: true },
723
+ delta_column: {
724
+ field: 'series',
725
+ name: 'test_variant'
726
+ }
727
+ }
728
+ };
729
+
730
+ highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('testvariant' + highchartsRenderer.delimer + 'other');
731
+
732
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
733
+ let result = fn.call(funcContext);
734
+
735
+ expect(result).toBe('12,345.678');
736
+ });
737
+ });
738
+
739
+ describe('Label options handling', () => {
740
+ it('should return raw value when show_out_of_x_axis is true but percentage logic is not triggered', () => {
741
+ opts = {
742
+ chartOptions: {
743
+ label: {
744
+ show_value: true,
745
+ show_out_of_x_axis: true
746
+ }
747
+ }
748
+ };
749
+
750
+ funcContext.y = 250;
751
+ mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
752
+
753
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
754
+ let result = fn.call(funcContext);
755
+
756
+ expect(result).toBe('250');
757
+ });
758
+
759
+ it('should return raw value when show_out_of_data_series is true but percentage logic is not triggered', () => {
760
+ opts = {
761
+ chartOptions: {
762
+ label: {
763
+ show_value: true,
764
+ show_out_of_data_series: true
765
+ }
766
+ }
767
+ };
768
+
769
+ funcContext.y = 200;
770
+ mockPivotData.getAggregator = jest.fn(() => ({ value: () => 800 }));
771
+
772
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
773
+ let result = fn.call(funcContext);
774
+
775
+ expect(result).toBe('200');
776
+ });
777
+
778
+ it('should return raw value when both percentage options are enabled but percentage logic is not triggered', () => {
779
+ opts = {
780
+ chartOptions: {
781
+ label: {
782
+ show_value: true,
783
+ show_out_of_x_axis: true,
784
+ show_out_of_data_series: true
785
+ }
786
+ }
787
+ };
788
+
789
+ funcContext.y = 250;
790
+ let callCount = 0;
791
+ mockPivotData.getAggregator = jest.fn(() => {
792
+ callCount++;
793
+ return { value: () => callCount === 1 ? 1000 : 800 };
794
+ });
795
+
796
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
797
+ let result = fn.call(funcContext);
798
+
799
+ expect(result).toBe('250');
800
+ });
801
+
802
+ it('should show only percentages without value when show_value is false but percentages are enabled', () => {
803
+ opts = {
804
+ chartOptions: {
805
+ label: {
806
+ show_value: false,
807
+ show_out_of_x_axis: true
808
+ }
809
+ }
810
+ };
811
+
812
+ funcContext.y = 250;
813
+ mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
814
+
815
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
816
+ let result = fn.call(funcContext);
817
+
818
+ expect(result).toBe('(25%)');
819
+ });
820
+
821
+ it('should not add percentage when value is falsy', () => {
822
+ opts = {
823
+ chartOptions: {
824
+ label: {
825
+ show_value: true,
826
+ show_out_of_x_axis: true
827
+ }
828
+ }
829
+ };
830
+
831
+ funcContext.y = 0;
832
+ mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
833
+
834
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
835
+ let result = fn.call(funcContext);
836
+
837
+ expect(result).toBe('0');
838
+ });
839
+
840
+ it('should not add percentage when axisTotal is falsy', () => {
841
+ opts = {
842
+ chartOptions: {
843
+ label: {
844
+ show_value: true,
845
+ show_out_of_x_axis: true
846
+ }
847
+ }
848
+ };
849
+
850
+ funcContext.y = 250;
851
+ mockPivotData.getAggregator = jest.fn(() => ({ value: () => 0 }));
852
+
853
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
854
+ let result = fn.call(funcContext);
855
+
856
+ expect(result).toBe('250');
857
+ });
858
+ });
859
+
860
+ describe('Object column handling', () => {
861
+ it('should extract name property from object columns', () => {
862
+ highchartsRenderer.getColsInFormatterContext.and.returnValue({ name: 'ObjectColumn' });
863
+
864
+ opts = { chartOptions: { label: { show_value: true } } };
865
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
866
+ let result = fn.call(funcContext);
867
+
868
+ expect(result).toBe('12,345.678');
869
+ });
870
+ });
871
+
872
+ describe('Column initialization handling', () => {
873
+ it('should initialize cols to empty array when cols is falsy after array check', () => {
874
+ spyOn(lodash, 'isArray').and.returnValue(true);
875
+ highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
876
+
877
+ opts = { chartOptions: { label: { show_value: true } } };
878
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
879
+ let result = fn.call(funcContext);
880
+
881
+ expect(result).toBe('12,345.678');
882
+ expect(lodash.isArray).toHaveBeenCalledWith(null);
883
+ });
884
+ });
885
+
886
+ describe('Waterfall breakdown handling', () => {
887
+ beforeEach(() => {
888
+ funcContext.series.options.className = 'waterfallBreakdown';
889
+ });
890
+
891
+ it('should transform rows and cols for waterfall breakdown', () => {
892
+ opts = { chartOptions: { label: { show_value: true } } };
893
+
894
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
895
+ let result = fn.call(funcContext);
896
+
897
+ expect(highchartsRenderer.transformRowsAndColsForBreakdown).toHaveBeenCalled();
898
+ expect(result).toBe('12,345.678');
899
+ });
900
+
901
+ it('should return raw value for waterfall breakdown when show_out_of_data_series is true but percentage logic is not triggered', () => {
902
+ opts = {
903
+ chartOptions: {
904
+ label: {
905
+ show_value: true,
906
+ show_out_of_data_series: true
907
+ }
908
+ }
909
+ };
910
+
911
+ funcContext.y = 300;
912
+ mockPivotData.getRowKeys = jest.fn(() => ([['row1'], ['row2']]));
913
+
914
+ let callCount = 0;
915
+ mockPivotData.getAggregator = jest.fn((rows, cols) => {
916
+ if (rows.length === 1) return { value: () => 400 };
917
+ if (rows.length === 0 && cols.length > 0) return { value: () => 1200 };
918
+ return { value: () => 800 };
919
+ });
920
+
921
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
922
+ let result = fn.call(funcContext);
923
+
924
+ expect(result).toBe('300');
925
+ });
926
+
927
+ it('should calculate percentage using dataSeriesTotal when axisTotal is falsy (waterfall breakdown)', () => {
928
+ opts = {
929
+ chartOptions: {
930
+ label: {
931
+ show_value: false,
932
+ show_out_of_data_series: true
933
+ }
934
+ }
935
+ };
936
+
937
+ funcContext.y = 200; // value = 200
938
+ mockPivotData.getRowKeys = jest.fn(() => ([['row1'], ['row2']]));
939
+
940
+ let callCount = 0;
941
+ mockPivotData.getAggregator = jest.fn((rows, cols) => {
942
+ callCount++;
943
+
944
+ if (rows.length === 1) {
945
+ return { value: () => 400 };
946
+ }
947
+ if (rows.length === 0 && cols.length > 0) {
948
+ return { value: () => 0 };
949
+ }
950
+
951
+ return { value: () => 200 };
952
+ });
953
+
954
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
955
+ let result = fn.call(funcContext);
956
+
957
+ expect(result).toBe('(25%)');
958
+ });
959
+
960
+ it('should not add percentage when dataSeriesTotal is falsy (non-waterfall breakdown)', () => {
961
+ funcContext.series.options.className = 'regularSeries';
962
+
963
+ opts = {
964
+ chartOptions: {
965
+ label: {
966
+ show_value: false,
967
+ show_out_of_data_series: true
968
+ }
969
+ }
970
+ };
971
+
972
+ funcContext.y = 200;
973
+
974
+ mockPivotData.getAggregator = jest.fn((rows, cols) => {
975
+ if (cols.length === 0) {
976
+ return { value: () => 0 };
977
+ }
978
+ return { value: () => 200 };
979
+ });
980
+
981
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
982
+ let result = fn.call(funcContext);
983
+
984
+ expect(result).toBe('');
985
+ });
986
+ });
987
+
988
+ describe('Error handling', () => {
989
+ it('should fallback to basic formatting when aggregator throws error', () => {
990
+ mockPivotData.getAggregator = jest.fn(() => { throw new Error('Test error'); });
991
+
992
+ opts = { chartOptions: { label: { show_value: true } } };
993
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
994
+ let result = fn.call(funcContext);
995
+
996
+ expect(result).toBe('12,345.678');
997
+ });
998
+
999
+ it('should handle null columns gracefully', () => {
1000
+ highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
1001
+
1002
+ opts = { chartOptions: { label: { show_value: true } } };
1003
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
1004
+ let result = fn.call(funcContext);
1005
+
1006
+ expect(result).toBe('12,345.678');
1007
+ });
1008
+
1009
+ it('should handle empty options gracefully for drill-down pie', () => {
1010
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, {}, true);
1011
+ let result = fn.call(funcContext);
1012
+
1013
+ expect(result).toBe('12,345.678');
1014
+ });
1015
+ });
1016
+
1017
+ describe('Unit abbreviation options (fallback behavior)', () => {
1018
+ it('should fallback to raw value formatting when useUnitAbbreviation is false and getFormattedNumber returns abbreviated value', () => {
1019
+ opts = {
1020
+ chartOptions: {
1021
+ label: {
1022
+ show_value: true,
1023
+ useUnitAbbreviation: false
1024
+ }
1025
+ }
1026
+ };
1027
+
1028
+ jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('12.5K');
1029
+
1030
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
1031
+ let result = fn.call(funcContext);
1032
+
1033
+ expect(result).toBe('12,345.678');
1034
+ });
1035
+
1036
+ it('should fallback to raw value formatting when useUnitAbbreviation is true and getFormattedNumber returns abbreviated value', () => {
1037
+ opts = {
1038
+ chartOptions: {
1039
+ label: {
1040
+ show_value: true,
1041
+ useUnitAbbreviation: true
1042
+ }
1043
+ }
1044
+ };
1045
+
1046
+ jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('12.5K');
1047
+
1048
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
1049
+ let result = fn.call(funcContext);
1050
+
1051
+ expect(result).toBe('12,345.678');
1052
+ });
1053
+
1054
+ it('should fallback to raw value formatting when getFormattedNumber returns M-abbreviated value regardless of useUnitAbbreviation setting', () => {
1055
+ opts = {
1056
+ chartOptions: {
1057
+ label: {
1058
+ show_value: true,
1059
+ useUnitAbbreviation: false
1060
+ }
1061
+ }
1062
+ };
1063
+
1064
+ jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('1.2M');
1065
+
1066
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
1067
+ let result = fn.call(funcContext);
1068
+
1069
+ expect(result).toBe('12,345.678');
1070
+ });
1071
+ });
1072
+
1073
+ describe('Others name handling', () => {
1074
+ it('should handle others name replacement', () => {
1075
+ opts = {
1076
+ total_value_options: { some: 'option' },
1077
+ chartOptions: { label: { show_value: true } }
1078
+ };
1079
+
1080
+ highchartsRenderer.getOthersName.and.returnValue('CustomOthers');
1081
+ highchartsRenderer.getDrOthersInAxisState.and.returnValue({ cols: true });
1082
+
1083
+ let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
1084
+ let result = fn.call(funcContext);
1085
+
1086
+ expect(highchartsRenderer.getOthersName).toHaveBeenCalledWith(opts);
1087
+ expect(highchartsRenderer.getDrOthersInAxisState).toHaveBeenCalledWith(mockPivotData, 'CustomOthers');
1088
+ expect(highchartsRenderer.replaceDrOthersKeys).toHaveBeenCalled();
1089
+ expect(result).toBe('12,345.678');
1090
+ });
527
1091
  });
528
1092
  });
529
1093
 
@@ -1508,7 +2072,7 @@ describe('highcharts_renderer', () => {
1508
2072
  field: 'series',
1509
2073
  name: 'TEST_test',
1510
2074
  same_yaxis: true,
1511
- is_percentage: true,
2075
+ is_percentage: false,
1512
2076
  }
1513
2077
  }
1514
2078
  };
@@ -1522,10 +2086,10 @@ describe('highcharts_renderer', () => {
1522
2086
  highchartsRenderer.updateBackwardCompatibleWidgetOptions(currentOptions, null);
1523
2087
  expect(currentOptions.comboOptions).toEqual({
1524
2088
  secondaryAxisSettings: {
1525
- name: 'TESTtest',
2089
+ name: 'Secondary Axis',
1526
2090
  max: null,
1527
2091
  min: null,
1528
- is_percentage: true
2092
+ is_percentage: false
1529
2093
  },
1530
2094
  seriesOptions: [{
1531
2095
  series: 'TEST_test',
@@ -2064,17 +2628,20 @@ describe('highcharts_renderer', () => {
2064
2628
  it('Should return General format if there are no widget_values_format', () => {
2065
2629
  aggregatorObject.widget_values_format = null;
2066
2630
  expect(aggregatorObject.format(123.4567, false)).toBe('123.46');
2631
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2067
2632
  });
2068
2633
 
2069
2634
  it('Should return widget format if it\'s not calculated value', () => {
2070
2635
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2071
2636
  expect(aggregatorObject.format(1123.4567, false)).toBe('$1,123.457');
2637
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2072
2638
  });
2073
2639
 
2074
2640
  it('Should return calculated value format if it\'s calculated value', () => {
2075
2641
  aggregator = highchartsRenderer.rhPivotAggregatorSum(arg, widget_values_format, is_graph, render_options, calculated_info);
2076
2642
  aggregatorObject = aggregator({}, ['Region average'], '');
2077
2643
  expect(aggregatorObject.format(1123.45678, false)).toBe('112345.68%');
2644
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2078
2645
  });
2079
2646
 
2080
2647
  it('if FF enabledNewWidgetValueFormatting is and some of secondaryAxis is true widget values format must be from seriesOptions and widget_value_format to equal first seriesOptions format', () => {
@@ -2091,6 +2658,7 @@ describe('highcharts_renderer', () => {
2091
2658
  aggregatorObject = aggregator({}, ['Profit'], '');
2092
2659
  aggregatorObject.push({ DR_Values: 'Profit', Profit: 123 });
2093
2660
  expect(aggregatorObject.format(1123.45678, false)).toBe('$1,123.457');
2661
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2094
2662
  });
2095
2663
  });
2096
2664
  });
@@ -2265,17 +2833,20 @@ describe('highcharts_renderer', () => {
2265
2833
  it('Should return General format if there are no widget_values_format', () => {
2266
2834
  aggregatorObject.widget_values_format = null;
2267
2835
  expect(aggregatorObject.format(123.4567, false)).toBe('123.46');
2836
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2268
2837
  });
2269
2838
 
2270
2839
  it('Should return widget format if it\'s not calculated value', () => {
2271
2840
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2272
2841
  expect(aggregatorObject.format(1123.4567, false)).toBe('$1,123.457');
2842
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2273
2843
  });
2274
2844
 
2275
2845
  it('Should return calculated value format if it\'s calculated value', () => {
2276
2846
  aggregator = highchartsRenderer.rhPivotCount(arg, widget_values_format, is_graph, render_options, calculated_info);
2277
2847
  aggregatorObject = aggregator({}, ['Region average'], '');
2278
2848
  expect(aggregatorObject.format(1123.45678, false)).toBe('112345.68%');
2849
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2279
2850
  });
2280
2851
 
2281
2852
  it('if FF enabledNewWidgetValueFormatting is and some of secondaryAxis is true widget values format must be from seriesOptions and widget_value_format to equal first seriesOptions format', () => {
@@ -2292,6 +2863,7 @@ describe('highcharts_renderer', () => {
2292
2863
  aggregatorObject = aggregator({}, ['Profit'], '');
2293
2864
  aggregatorObject.push({ DR_Values: 'Profit', Profit: 123 });
2294
2865
  expect(aggregatorObject.format(1123.45678, false)).toBe('$1,123.457');
2866
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2295
2867
  });
2296
2868
  });
2297
2869
  });
@@ -2424,6 +2996,7 @@ describe('highcharts_renderer', () => {
2424
2996
  it('Should return widget format if it\'s graph', () => {
2425
2997
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2426
2998
  expect(aggregatorObject.format(2, false)).toBe('$2.000');
2999
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2427
3000
  });
2428
3001
 
2429
3002
  it('Should return widget format if it\'s only_formats', () => {
@@ -2431,6 +3004,7 @@ describe('highcharts_renderer', () => {
2431
3004
  aggregatorObject = aggregator({}, '', '');
2432
3005
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2433
3006
  expect(aggregatorObject.format(2, true)).toBe('$2.000');
3007
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2434
3008
  });
2435
3009
 
2436
3010
  it('Should return uniq values if it\'s table and not only_formats', () => {
@@ -2438,6 +3012,7 @@ describe('highcharts_renderer', () => {
2438
3012
  aggregatorObject = aggregator({}, '', '');
2439
3013
  aggregatorObject.formated_values = ['val1', 'val2'];
2440
3014
  expect(aggregatorObject.format(aggregatorObject.formated_values, false)).toBe('val1<br>val2');
3015
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2441
3016
  });
2442
3017
  });
2443
3018
  });
@@ -2616,17 +3191,20 @@ describe('highcharts_renderer', () => {
2616
3191
  it('Should return General format if there are no widget_values_format', () => {
2617
3192
  aggregatorObject.widget_values_format = null;
2618
3193
  expect(aggregatorObject.format(123.4567, false)).toBe('123.46');
3194
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2619
3195
  });
2620
3196
 
2621
3197
  it('Should return widget format if it\'s not calculated value', () => {
2622
3198
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2623
3199
  expect(aggregatorObject.format(1123.4567, false)).toBe('$1,123.457');
3200
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2624
3201
  });
2625
3202
 
2626
3203
  it('Should return calculated value format if it\'s calculated value', () => {
2627
3204
  aggregator = highchartsRenderer.rhPivotAggregatorAverage(arg, widget_values_format, is_graph, render_options, calculated_info);
2628
3205
  aggregatorObject = aggregator({}, ['Region average'], '');
2629
3206
  expect(aggregatorObject.format(1123.45678, false)).toBe('112345.68%');
3207
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2630
3208
  });
2631
3209
 
2632
3210
  it('if FF enabledNewWidgetValueFormatting is and some of secondaryAxis is true widget values format must be from seriesOptions and widget_value_format to equal first seriesOptions format', () => {
@@ -2643,6 +3221,7 @@ describe('highcharts_renderer', () => {
2643
3221
  aggregatorObject = aggregator({}, ['Profit'], '');
2644
3222
  aggregatorObject.push({ DR_Values: 'Profit', Profit: 123 });
2645
3223
  expect(aggregatorObject.format(1123.45678, false)).toBe('$1,123.457');
3224
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2646
3225
  });
2647
3226
  });
2648
3227
  });
@@ -2818,17 +3397,20 @@ describe('highcharts_renderer', () => {
2818
3397
  it('Should return General format if there are no widget_values_format', () => {
2819
3398
  aggregatorObject.widget_values_format = null;
2820
3399
  expect(aggregatorObject.format(123.4567, false)).toBe('123.46');
3400
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2821
3401
  });
2822
3402
 
2823
3403
  it('Should return widget format if it\'s not calculated value', () => {
2824
3404
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
2825
3405
  expect(aggregatorObject.format(1123.4567, false)).toBe('$1,123.457');
3406
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2826
3407
  });
2827
3408
 
2828
3409
  it('Should return calculated value format if it\'s calculated value', () => {
2829
3410
  aggregator = highchartsRenderer.rhPivotAggregatorMin(arg, widget_values_format, is_graph, render_options, calculated_info);
2830
3411
  aggregatorObject = aggregator({}, ['Region average'], '');
2831
3412
  expect(aggregatorObject.format(1123.45678, false)).toBe('112345.68%');
3413
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2832
3414
  });
2833
3415
 
2834
3416
  it('if FF enabledNewWidgetValueFormatting is and some of secondaryAxis is true widget values format must be from seriesOptions and widget_value_format to equal first seriesOptions format', () => {
@@ -2845,6 +3427,7 @@ describe('highcharts_renderer', () => {
2845
3427
  aggregatorObject = aggregator({}, ['Profit'], '');
2846
3428
  aggregatorObject.push({ DR_Values: 'Profit', Profit: 123 });
2847
3429
  expect(aggregatorObject.format(1123.45678, false)).toBe('$1,123.457');
3430
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
2848
3431
  });
2849
3432
  });
2850
3433
  });
@@ -3020,17 +3603,20 @@ describe('highcharts_renderer', () => {
3020
3603
  it('Should return General format if there are no widget_values_format', () => {
3021
3604
  aggregatorObject.widget_values_format = null;
3022
3605
  expect(aggregatorObject.format(123.4567, false)).toBe('123.46');
3606
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
3023
3607
  });
3024
3608
 
3025
3609
  it('Should return widget format if it\'s not calculated value', () => {
3026
3610
  aggregatorObject.widget_values_format = '\"$\"#,###.###';
3027
3611
  expect(aggregatorObject.format(1123.4567, false)).toBe('$1,123.457');
3612
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
3028
3613
  });
3029
3614
 
3030
3615
  it('Should return calculated value format if it\'s calculated value', () => {
3031
3616
  aggregator = highchartsRenderer.rhPivotAggregatorMax(arg, widget_values_format, is_graph, render_options, calculated_info);
3032
3617
  aggregatorObject = aggregator({}, ['Region average'], '');
3033
3618
  expect(aggregatorObject.format(1123.45678, false)).toBe('112345.68%');
3619
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
3034
3620
  });
3035
3621
 
3036
3622
  it('if FF enabledNewWidgetValueFormatting is and some of secondaryAxis is true widget values format must be from seriesOptions and widget_value_format to equal first seriesOptions format', () => {
@@ -3047,6 +3633,7 @@ describe('highcharts_renderer', () => {
3047
3633
  aggregatorObject = aggregator({}, ['Profit'], '');
3048
3634
  aggregatorObject.push({ DR_Values: 'Profit', Profit: 123 });
3049
3635
  expect(aggregatorObject.format(1123.45678, false)).toBe('$1,123.457');
3636
+ expect(getAggregatorPercentageValueIfRequiredMock).toHaveBeenCalled();
3050
3637
  });
3051
3638
  });
3052
3639
  });
@@ -3054,6 +3641,115 @@ describe('highcharts_renderer', () => {
3054
3641
  });
3055
3642
 
3056
3643
  describe('function getDefaultValueForChart', () => {
3644
+ let originalRich, originalGetter, originalSubDefaults;
3645
+
3646
+ beforeEach(() => {
3647
+ originalRich = highchartsRenderer.richTextSubType;
3648
+ originalGetter = highchartsRenderer.getChartOptionsBySubType;
3649
+ originalSubDefaults = highchartsRenderer.getDefaultValueForSubOptions;
3650
+
3651
+ highchartsRenderer.richTextSubType = { type: 'rich_text', suboptions: [], default_options: null };
3652
+ });
3653
+
3654
+ afterEach(() => {
3655
+ highchartsRenderer.richTextSubType = originalRich;
3656
+ highchartsRenderer.getChartOptionsBySubType = originalGetter;
3657
+ highchartsRenderer.getDefaultValueForSubOptions = originalSubDefaults;
3658
+ });
3659
+
3660
+ it('returns {} for unknown subtype', () => {
3661
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => null);
3662
+ expect(highchartsRenderer.getDefaultValueForChart('unknown-type', {})).toEqual({});
3663
+ });
3664
+
3665
+ it('deep-clones suboptions return value (no shared refs)', () => {
3666
+ const shared = { arr: [1, 2, 3], nested: { x: 1 } };
3667
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => shared);
3668
+
3669
+ const chartOpt = {
3670
+ suboptions: [{ category_type: 'segments' }, { category_type: 'label' }],
3671
+ default_options: {},
3672
+ };
3673
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => chartOpt);
3674
+
3675
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3676
+ expect(res.segments).toEqual(shared);
3677
+ expect(res.segments).not.toBe(shared);
3678
+
3679
+ res.segments.arr.push(99);
3680
+ res.segments.nested.x = 42;
3681
+ expect(shared.arr).toEqual([1, 2, 3]);
3682
+ expect(shared.nested.x).toBe(1);
3683
+ });
3684
+
3685
+ it('does not mutate chartOpt.default_options (base defaults stay immutable)', () => {
3686
+ const baseDefaults = {
3687
+ segments: [10, 20],
3688
+ label: { font_size: 12, style: { weight: 400 } },
3689
+ };
3690
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({ label: { font_size: 8 } }));
3691
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3692
+ suboptions: [{ category_type: 'label' }],
3693
+ default_options: baseDefaults,
3694
+ }));
3695
+
3696
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3697
+ res.label.style.weight = 700;
3698
+ res.segments.push(30);
3699
+
3700
+ expect(baseDefaults).toEqual({
3701
+ segments: [10, 20],
3702
+ label: { font_size: 12, style: { weight: 400 } },
3703
+ });
3704
+ });
3705
+
3706
+ it('default_options override suboptions (old semantics) and arrays are replaced, not merged', () => {
3707
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn((sub) => {
3708
+ if (sub.category_type === 'segments') return [1, 2, 3];
3709
+ if (sub.category_type === 'label') return { font_size: 8 };
3710
+ return {};
3711
+ });
3712
+
3713
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3714
+ suboptions: [{ category_type: 'segments' }, { category_type: 'label' }],
3715
+ default_options: {
3716
+ segments: [100],
3717
+ label: { font_size: 12, color: '#000' },
3718
+ },
3719
+ }));
3720
+
3721
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3722
+ expect(res.segments).toEqual([100]); // массив заменён целиком
3723
+ expect(res.label).toEqual({ font_size: 12, color: '#000' }); // приоритет у default_options
3724
+ });
3725
+
3726
+ it('fresh object on each call (no cross-call leakage)', () => {
3727
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({ a: 1 }));
3728
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3729
+ suboptions: [{ category_type: 'cfg' }],
3730
+ default_options: {},
3731
+ }));
3732
+
3733
+ const res1 = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3734
+ const res2 = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3735
+ res1.cfg.a = 99;
3736
+
3737
+ expect(res2.cfg.a).toBe(1);
3738
+ expect(res1.cfg).not.toBe(res2.cfg);
3739
+ });
3740
+
3741
+ it('handles non-plain values inside defaults (e.g., Date) without throwing', () => {
3742
+ const dt = new Date('2025-01-01T00:00:00.000Z');
3743
+ highchartsRenderer.getDefaultValueForSubOptions = jest.fn(() => ({}));
3744
+ highchartsRenderer.getChartOptionsBySubType = jest.fn(() => ({
3745
+ suboptions: [],
3746
+ default_options: { createdAt: dt },
3747
+ }));
3748
+
3749
+ const res = highchartsRenderer.getDefaultValueForChart('line-chart', {});
3750
+ expect(new Date(res.createdAt).getTime()).toBe(dt.getTime());
3751
+ });
3752
+
3057
3753
  it('should return empty value for rich_text type', () => {
3058
3754
  expect(highchartsRenderer.getDefaultValueForChart('rich_text', {})).toEqual({});
3059
3755
  });
@@ -6854,7 +7550,9 @@ describe('highcharts_renderer', () => {
6854
7550
  ]);
6855
7551
  });
6856
7552
  it('should prepare appropriate chart series for standard input', () => {
6857
- opts = {};
7553
+ opts = {
7554
+ chartOptions: {}
7555
+ };
6858
7556
  chartType = null;
6859
7557
  const value = highchartsRenderer.ptCreateColumnSeries(pivotDataMock, colors, onlyNumbers, isUniqueVals, isNotDrillDown, additionalOptionsMock, opts, chartOptions, chartType);
6860
7558
  expect(value).toEqual([
@@ -7083,7 +7781,9 @@ describe('highcharts_renderer', () => {
7083
7781
  "#b3060e",
7084
7782
  "#70000a"
7085
7783
  ];
7086
- opts = {};
7784
+ opts = {
7785
+ chartOptions: {}
7786
+ };
7087
7787
  chartOptions = {
7088
7788
  chart: {
7089
7789
  type: '',
@@ -7241,7 +7941,8 @@ describe('highcharts_renderer', () => {
7241
7941
 
7242
7942
  it('should prepare appropriate chart series, when opts \'total\' configuration is set to true', ()=> {
7243
7943
  opts = {
7244
- total: true
7944
+ total: true,
7945
+ chartOptions: {}
7245
7946
  };
7246
7947
  pivotDataMock.colTotals = {
7247
7948
  'col 1': {value: () => 123450},
@@ -7310,6 +8011,7 @@ describe('highcharts_renderer', () => {
7310
8011
 
7311
8012
  it('should prepare appropriate chart series, when opts \'trendLine\' configuration is set to true', ()=> {
7312
8013
  opts = {
8014
+ chartOptions: {},
7313
8015
  trendLine: true
7314
8016
  };
7315
8017
  const value = highchartsRenderer.ptCreateBasicLineSeries(pivotDataMock, colors, onlyNumbers, isUniqueVals, additionalOptionsMock, opts, chartOptions);
@@ -7533,36 +8235,6 @@ describe('highcharts_renderer', () => {
7533
8235
  );
7534
8236
  });
7535
8237
 
7536
- it('should return no data result if series is empty', () => {
7537
- const chartOptions = {
7538
- chart: {},
7539
- series: [{ data: [] }, { data: [] }, {}],
7540
- };
7541
- const options = {};
7542
- const noDataFnSpy = jest.spyOn(highchartsRenderer, 'getNoDataResult').mockImplementation(() => {});
7543
-
7544
- highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
7545
-
7546
- expect(noDataFnSpy).toHaveBeenCalled();
7547
- expect(options.error_has_occurred).toBeTruthy();
7548
- expect(options.error_params).toBe(highchartsRenderer.widgetPlaceholders.nodata);
7549
- });
7550
-
7551
- it('should return too much data result if series is too long', () => {
7552
- const chartOptions = {
7553
- chart: {},
7554
- series: [{ data: new Array(1000) }, { data: new Array(1000) }, {}],
7555
- };
7556
- const options = {};
7557
- const noDataFnSpy = jest.spyOn(highchartsRenderer, 'getNoDataResult').mockImplementation(() => {});
7558
-
7559
- highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
7560
-
7561
- expect(noDataFnSpy).toHaveBeenCalled();
7562
- expect(options.error_has_occurred).toBeTruthy();
7563
- expect(options.error_params).toBe(highchartsRenderer.widgetPlaceholders.tooMuchData);
7564
- });
7565
-
7566
8238
  it('should set hcInstance on options with chart object for graph table renderer to use', () => {
7567
8239
  jest.useFakeTimers();
7568
8240
  jest.spyOn(Highcharts, 'chart').mockImplementation(() => ({ chart: true }));
@@ -7580,40 +8252,330 @@ describe('highcharts_renderer', () => {
7580
8252
  });
7581
8253
  });
7582
8254
 
7583
- describe('Function getNoDataResult', () => {
7584
- const container = $('<div class="noData-box"></div>');
8255
+ describe('Error Throwing Functionality', () => {
8256
+ const {
8257
+ NoDataError,
8258
+ TooMuchDataError,
8259
+ DataConflictError,
8260
+ GaugeConfigurationError,
8261
+ BaseRendererError,
8262
+ GenericRenderingError,
8263
+ GenericComputationalError
8264
+ } = require('../src/errors');
7585
8265
 
7586
- it('should return no data html', () => {
7587
- const placeholderMeta = highchartsRenderer.widgetPlaceholders.nodata;
7588
- const expected = container.clone().html(highchartsRenderer.getWidgetPlaceholder(placeholderMeta));
7589
- expect(highchartsRenderer.getNoDataResult()).toEqual(expected);
8266
+ beforeEach(() => {
8267
+ jest.clearAllMocks();
7590
8268
  });
7591
8269
 
7592
- it('should return too much data html', () => {
7593
- const placeholderMeta = highchartsRenderer.widgetPlaceholders.tooMuchData;
7594
- const expected = container.clone().html(highchartsRenderer.getWidgetPlaceholder(placeholderMeta));
7595
- expect(highchartsRenderer.getNoDataResult(true)).toEqual(expected);
8270
+ afterAll(() => {
8271
+ jest.restoreAllMocks();
7596
8272
  });
7597
8273
 
7598
- });
8274
+ describe('ptCreateElementAndDraw - Error Throwing', () => {
8275
+ it('should throw NoDataError when series is empty and not onlyText', () => {
8276
+ const chartOptions = {
8277
+ chart: {},
8278
+ series: [{ data: [] }, { data: [] }, {}],
8279
+ };
8280
+ const options = {};
8281
+
8282
+ expect(() => {
8283
+ highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
8284
+ }).toThrow(NoDataError);
8285
+ });
8286
+
8287
+ it('should throw TooMuchDataError when series has too much data', () => {
8288
+ const chartOptions = {
8289
+ chart: {},
8290
+ series: [{ data: new Array(1000) }, { data: new Array(1000) }, {}],
8291
+ };
8292
+ const options = {};
8293
+
8294
+ expect(() => {
8295
+ highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
8296
+ }).toThrow(TooMuchDataError);
8297
+ });
8298
+
8299
+ it('should not throw errors when onlyText is true even if no data', () => {
8300
+ const chartOptions = {
8301
+ chart: {},
8302
+ series: [{ data: [] }],
8303
+ onlyText: true,
8304
+ };
8305
+ const options = {};
8306
+
8307
+ expect(() => {
8308
+ highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
8309
+ }).not.toThrow();
8310
+ });
8311
+
8312
+ it('should process normally when series has valid data', () => {
8313
+ jest.spyOn(Highcharts, 'chart').mockImplementation(() => ({ chart: true }));
8314
+ jest.useFakeTimers();
8315
+
8316
+ const chartOptions = {
8317
+ chart: {},
8318
+ series: [{ data: [1, 2, 3] }],
8319
+ };
8320
+ const options = {};
7599
8321
 
7600
- describe('Function getWidgetPlaceholder', () => {
7601
- const titleSelector = '.noData-title';
7602
- const imageSelector = '.noData-image';
7603
- const textSelector = '.noData-text';
8322
+ expect(() => {
8323
+ highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
8324
+ }).not.toThrow();
8325
+
8326
+ jest.runAllTimers();
8327
+ jest.restoreAllMocks();
8328
+ jest.useRealTimers();
8329
+ });
8330
+ });
8331
+
8332
+ describe('Data Conflict Error Throwing', () => {
8333
+ it('should throw DataConflictError when categories are below minimum in waterfall chart', () => {
8334
+ const options = {
8335
+ isBreakdown: true,
8336
+ uniqueCategories: ['A', 'B'], // Only 2 categories
8337
+ minCategories: 5,
8338
+ maxCategories: 10
8339
+ };
8340
+
8341
+ expect(() => {
8342
+ throw new DataConflictError({
8343
+ isBreakdown: options.isBreakdown,
8344
+ uniqueCategories: options.uniqueCategories,
8345
+ minCategories: options.minCategories,
8346
+ maxCategories: options.maxCategories
8347
+ });
8348
+ }).toThrow(DataConflictError);
8349
+ });
8350
+
8351
+ it('should throw DataConflictError when categories exceed maximum in waterfall chart', () => {
8352
+ const uniqueCategories = new Array(15).fill(0).map((_, i) => `Category${i}`); // 15 categories
8353
+ const options = {
8354
+ isBreakdown: false,
8355
+ uniqueCategories,
8356
+ minCategories: 3,
8357
+ maxCategories: 10
8358
+ };
8359
+
8360
+ expect(() => {
8361
+ throw new DataConflictError({
8362
+ isBreakdown: options.isBreakdown,
8363
+ uniqueCategories: options.uniqueCategories,
8364
+ minCategories: options.minCategories,
8365
+ maxCategories: options.maxCategories
8366
+ });
8367
+ }).toThrow(DataConflictError);
8368
+ });
8369
+
8370
+ it('should create DataConflictError with correct options for breakdown scenario', () => {
8371
+ const options = {
8372
+ isBreakdown: true,
8373
+ uniqueCategories: ['A'],
8374
+ minCategories: 5,
8375
+ maxCategories: 10
8376
+ };
8377
+
8378
+ try {
8379
+ throw new DataConflictError(options);
8380
+ } catch (error) {
8381
+ expect(error).toBeInstanceOf(DataConflictError);
8382
+ expect(error.code).toBe(5);
8383
+ expect(error.title).toBe('Data Conflict');
8384
+ expect(error.options).toEqual(options);
8385
+ expect(error.options.isBreakdown).toBe(true);
8386
+ expect(error.options.uniqueCategories).toEqual(['A']);
8387
+ expect(error.options.minCategories).toBe(5);
8388
+ expect(error.options.maxCategories).toBe(10);
8389
+ }
8390
+ });
8391
+
8392
+ it('should create DataConflictError with correct options for walkthrough scenario', () => {
8393
+ const options = {
8394
+ isBreakdown: false,
8395
+ uniqueCategories: ['A', 'B', 'C'],
8396
+ minCategories: 5,
8397
+ maxCategories: 8
8398
+ };
8399
+
8400
+ try {
8401
+ throw new DataConflictError(options);
8402
+ } catch (error) {
8403
+ expect(error).toBeInstanceOf(DataConflictError);
8404
+ expect(error.code).toBe(5);
8405
+ expect(error.title).toBe('Data Conflict');
8406
+ expect(error.options).toEqual(options);
8407
+ expect(error.options.isBreakdown).toBe(false);
8408
+ }
8409
+ });
8410
+ });
8411
+
8412
+ describe('Gauge Configuration Error Throwing', () => {
8413
+ it('should create GaugeConfigurationError with correct properties', () => {
8414
+ try {
8415
+ throw new GaugeConfigurationError();
8416
+ } catch (error) {
8417
+ expect(error).toBeInstanceOf(GaugeConfigurationError);
8418
+ expect(error.code).toBe(6);
8419
+ expect(error.title).toBe('Please configure goal and needle');
8420
+ expect(error.options).toEqual({});
8421
+ }
8422
+ });
7604
8423
 
7605
- it('should return default placeholder when no data is provided', () => {
7606
- const placeholder = highchartsRenderer.getWidgetPlaceholder();
7607
- const defaultPlaceholder = '<div class="noData"><i class="noData-image"></i> no data</div>';
7608
- expect(placeholder[0].outerHTML).toEqual(defaultPlaceholder);
8424
+ it('should be instance of BaseRendererError', () => {
8425
+ const error = new GaugeConfigurationError();
8426
+ expect(error).toBeInstanceOf(require('../src/errors').BaseRendererError);
8427
+ });
7609
8428
  });
7610
8429
 
7611
- it('should return placeholder html when data is provided', () => {
7612
- Object.entries(highchartsRenderer.widgetPlaceholders).forEach(([, value]) => {
7613
- const placeholder = highchartsRenderer.getWidgetPlaceholder(value)[0];
7614
- expect(placeholder.querySelector(titleSelector).innerHTML).toEqual(value.title);
7615
- expect(placeholder.querySelector(imageSelector).classList.contains(value.class)).toBeTruthy();
7616
- expect(placeholder.querySelector(textSelector).innerHTML).toEqual(value.text);
8430
+ describe('Error handling in rhPivotView functions (testing private _handleComputationalError and _handleRenderingError)', () => {
8431
+ beforeEach(() => {
8432
+ jest.spyOn(console, 'error').mockImplementation(() => {});
8433
+ });
8434
+
8435
+ afterEach(() => {
8436
+ jest.restoreAllMocks();
8437
+ });
8438
+
8439
+ describe('Natural error conditions (testing private error handlers indirectly)', () => {
8440
+ it('should handle too much data error correctly', () => {
8441
+ // Create dataset that exceeds MAX_ROWS_FOR_SHOW_RESULTS to trigger TooMuchDataError
8442
+ const largeRowData = new Array(highchartsRenderer.MAX_ROWS_FOR_SHOW_RESULTS + 1)
8443
+ .fill({ field1: 'value1', field2: 'value2' });
8444
+
8445
+ const options = {
8446
+ onlyOptions: false,
8447
+ rendererOptions: {},
8448
+ renderer: jest.fn()
8449
+ };
8450
+
8451
+ expect(() => {
8452
+ highchartsRenderer.rhPivotView(largeRowData, options);
8453
+ }).toThrow(TooMuchDataError);
8454
+ });
8455
+
8456
+ it('should return empty object for too much data when onlyOptions is true', () => {
8457
+ // Create dataset that exceeds MAX_ROWS_FOR_SHOW_RESULTS
8458
+ const largeRowData = new Array(highchartsRenderer.MAX_ROWS_FOR_SHOW_RESULTS + 1)
8459
+ .fill({ field1: 'value1', field2: 'value2' });
8460
+
8461
+ const options = {
8462
+ onlyOptions: true,
8463
+ rendererOptions: {},
8464
+ renderer: jest.fn()
8465
+ };
8466
+
8467
+ const result = highchartsRenderer.rhPivotView(largeRowData, options);
8468
+ expect(result).toEqual({});
8469
+ });
8470
+
8471
+ it('should handle no data error correctly', () => {
8472
+ const options = {
8473
+ onlyOptions: false,
8474
+ rendererOptions: {},
8475
+ renderer: jest.fn()
8476
+ };
8477
+
8478
+ // Empty rowData should trigger NoDataError
8479
+ expect(() => {
8480
+ highchartsRenderer.rhPivotView([], options);
8481
+ }).toThrow(NoDataError);
8482
+ });
8483
+
8484
+ it('should return empty object for no data when onlyOptions is true', () => {
8485
+ const options = {
8486
+ onlyOptions: true,
8487
+ rendererOptions: {},
8488
+ renderer: jest.fn()
8489
+ };
8490
+
8491
+ const result = highchartsRenderer.rhPivotView([], options);
8492
+ expect(result).toEqual({});
8493
+ });
8494
+
8495
+ it('should handle renderer errors correctly', () => {
8496
+ const rowData = [{ field1: 'value1', field2: 'value2' }];
8497
+ const genericError = new Error('Renderer failed');
8498
+
8499
+ const options = {
8500
+ onlyOptions: false,
8501
+ rendererOptions: {},
8502
+ renderer: jest.fn().mockImplementation(() => {
8503
+ throw genericError;
8504
+ })
8505
+ };
8506
+
8507
+ // Generic errors from renderer should be wrapped in GenericRenderingError
8508
+ expect(() => {
8509
+ highchartsRenderer.rhPivotView(rowData, options);
8510
+ }).toThrow(GenericRenderingError);
8511
+ });
8512
+
8513
+ it('should return empty object for renderer errors when onlyOptions is true', () => {
8514
+ const rowData = [{ field1: 'value1', field2: 'value2' }];
8515
+ const genericError = new Error('Renderer failed');
8516
+
8517
+ const options = {
8518
+ onlyOptions: true,
8519
+ rendererOptions: {},
8520
+ renderer: jest.fn().mockImplementation(() => {
8521
+ throw genericError;
8522
+ })
8523
+ };
8524
+
8525
+ const result = highchartsRenderer.rhPivotView(rowData, options);
8526
+ expect(result).toEqual({});
8527
+ });
8528
+
8529
+ it('should re-throw BaseRendererError instances from renderer unchanged', () => {
8530
+ const rowData = [{ field1: 'value1', field2: 'value2' }];
8531
+ const originalError = new NoDataError();
8532
+
8533
+ const options = {
8534
+ onlyOptions: false,
8535
+ rendererOptions: {},
8536
+ renderer: jest.fn().mockImplementation(() => {
8537
+ throw originalError;
8538
+ })
8539
+ };
8540
+
8541
+ // BaseRendererError instances should be re-thrown unchanged
8542
+ expect(() => {
8543
+ highchartsRenderer.rhPivotView(rowData, options);
8544
+ }).toThrow(NoDataError);
8545
+
8546
+ expect(() => {
8547
+ highchartsRenderer.rhPivotView(rowData, options);
8548
+ }).toThrow(originalError);
8549
+ });
8550
+
8551
+ it('should handle null/undefined from renderer as GenericRenderingError', () => {
8552
+ const rowData = [{ field1: 'value1', field2: 'value2' }];
8553
+
8554
+ // Test null
8555
+ const nullOptions = {
8556
+ onlyOptions: false,
8557
+ rendererOptions: {},
8558
+ renderer: jest.fn().mockImplementation(() => {
8559
+ throw null;
8560
+ })
8561
+ };
8562
+
8563
+ expect(() => {
8564
+ highchartsRenderer.rhPivotView(rowData, nullOptions);
8565
+ }).toThrow(GenericRenderingError);
8566
+
8567
+ const undefinedOptions = {
8568
+ onlyOptions: false,
8569
+ rendererOptions: {},
8570
+ renderer: jest.fn().mockImplementation(() => {
8571
+ throw undefined;
8572
+ })
8573
+ };
8574
+
8575
+ expect(() => {
8576
+ highchartsRenderer.rhPivotView(rowData, undefinedOptions);
8577
+ }).toThrow(GenericRenderingError);
8578
+ });
7617
8579
  });
7618
8580
  });
7619
8581
  });