@datarailsshared/dr_renderer 1.4.65 → 1.4.78
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/README.md +38 -0
- package/package.json +1 -1
- package/src/dr_pivottable.js +14 -25
- package/src/errors.js +174 -0
- package/src/highcharts_renderer.js +80 -108
- package/src/index.d.ts +11 -0
- package/src/index.js +11 -0
- package/src/pivot.css +0 -11
- package/src/pivottable.js +14 -57
- package/src/types/errors.d.ts +120 -0
- package/src/types/index.d.ts +2 -1
- package/tests/errors.test.js +157 -0
- package/tests/highcharts_renderer.test.js +861 -61
- package/tests/mock/widgets.json +1 -3
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +2 -1
@@ -511,19 +511,559 @@ describe('highcharts_renderer', () => {
|
|
511
511
|
});
|
512
512
|
|
513
513
|
describe('function defaultDataLabelFormatter', () => {
|
514
|
+
let mockPivotData;
|
514
515
|
let funcContext;
|
515
516
|
let opts;
|
516
517
|
|
517
518
|
beforeEach(() => {
|
518
519
|
highchartsRenderer.enabledNewWidgetValueFormatting = false;
|
519
|
-
|
520
|
-
|
520
|
+
highchartsRenderer.delimer = ' , ';
|
521
|
+
|
522
|
+
funcContext = {
|
523
|
+
y: 12345.678,
|
524
|
+
series: {
|
525
|
+
name: 'TestSeries',
|
526
|
+
userOptions: {},
|
527
|
+
options: {}
|
528
|
+
},
|
529
|
+
point: {
|
530
|
+
name: 'TestPoint',
|
531
|
+
options: {}
|
532
|
+
}
|
533
|
+
};
|
534
|
+
opts = {};
|
535
|
+
|
536
|
+
mockPivotData = {
|
537
|
+
rowAttrs: ['row1'],
|
538
|
+
colAttrs: ['col1'],
|
539
|
+
getRowKeys: jest.fn(() => [['row1'], ['row2']]),
|
540
|
+
getAggregator: jest.fn(() => ({
|
541
|
+
value: () => 1000
|
542
|
+
}))
|
543
|
+
};
|
544
|
+
|
545
|
+
spyOn(highchartsRenderer, 'getSeriesNameInFormatterContext').and.returnValue('TestSeries');
|
546
|
+
spyOn(highchartsRenderer, 'getColsInFormatterContext').and.returnValue(['col1']);
|
547
|
+
spyOn(highchartsRenderer, 'getOthersName').and.returnValue('Others');
|
548
|
+
spyOn(highchartsRenderer, 'getDrOthersInAxisState').and.returnValue({});
|
549
|
+
spyOn(highchartsRenderer, 'transformRowsAndColsForBreakdown').and.returnValue({
|
550
|
+
rows: ['row1'],
|
551
|
+
cols: ['col1']
|
552
|
+
});
|
553
|
+
spyOn(highchartsRenderer, 'replaceDrOthersKeys');
|
554
|
+
spyOn(highchartsRenderer, 'selfStartsWith').and.returnValue(false);
|
555
|
+
|
556
|
+
global.$ = {
|
557
|
+
pivotUtilities: {
|
558
|
+
getFormattedNumber: jest.fn(() => '1,234.56')
|
559
|
+
}
|
560
|
+
};
|
521
561
|
});
|
522
562
|
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
563
|
+
describe('No pivotData is provided', () => {
|
564
|
+
it('should return formatted number as local string', () => {
|
565
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(null, {});
|
566
|
+
let result = fn.call(funcContext);
|
567
|
+
|
568
|
+
expect(result).toBe('12,345.678');
|
569
|
+
});
|
570
|
+
|
571
|
+
it('should handle unit sign removal when labelOptions are provided', () => {
|
572
|
+
const labelOptions = { useUnitAbbreviation: false };
|
573
|
+
opts = { chartOptions: { label: labelOptions } };
|
574
|
+
|
575
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(null, opts);
|
576
|
+
let result = fn.call(funcContext);
|
577
|
+
|
578
|
+
expect(result).toBe('12,345.678');
|
579
|
+
});
|
580
|
+
});
|
581
|
+
|
582
|
+
describe('pivotData is provided', () => {
|
583
|
+
beforeEach(() => {
|
584
|
+
funcContext.y = 1234.56;
|
585
|
+
});
|
586
|
+
|
587
|
+
it('should format value using pivot data aggregator when show_value is true', () => {
|
588
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
589
|
+
|
590
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
591
|
+
let result = fn.call(funcContext);
|
592
|
+
|
593
|
+
expect(highchartsRenderer.getSeriesNameInFormatterContext).toHaveBeenCalledWith(funcContext);
|
594
|
+
expect(highchartsRenderer.getColsInFormatterContext).toHaveBeenCalledWith(funcContext);
|
595
|
+
expect(mockPivotData.getAggregator).toHaveBeenCalled();
|
596
|
+
expect(result).toBe('1,234.56');
|
597
|
+
});
|
598
|
+
|
599
|
+
it('should return empty string when show_value is false and not drill-down pie', () => {
|
600
|
+
opts = { chartOptions: { label: { show_value: false } } };
|
601
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
602
|
+
let result = fn.call(funcContext);
|
603
|
+
|
604
|
+
expect(result).toBe('');
|
605
|
+
});
|
606
|
+
|
607
|
+
it('should handle empty row attributes', () => {
|
608
|
+
mockPivotData.rowAttrs = [];
|
609
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
610
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
611
|
+
let result = fn.call(funcContext);
|
612
|
+
|
613
|
+
expect(result).toBe('1,234.56');
|
614
|
+
});
|
615
|
+
|
616
|
+
it('should handle totalSeries className', () => {
|
617
|
+
funcContext.series.options.className = 'totalSeries';
|
618
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
619
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
620
|
+
let result = fn.call(funcContext);
|
621
|
+
|
622
|
+
expect(result).toBe('1,234.56');
|
623
|
+
});
|
624
|
+
|
625
|
+
it('should handle trendSeries className', () => {
|
626
|
+
funcContext.series.options.className = 'trendSeries';
|
627
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
628
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
629
|
+
let result = fn.call(funcContext);
|
630
|
+
|
631
|
+
expect(result).toBe('1,234.56');
|
632
|
+
});
|
633
|
+
});
|
634
|
+
|
635
|
+
describe('Drill-down pie chart scenarios', () => {
|
636
|
+
beforeEach(() => {
|
637
|
+
funcContext.y = 500;
|
638
|
+
});
|
639
|
+
|
640
|
+
it('should handle drill-down pie when series name starts with "Series "', () => {
|
641
|
+
highchartsRenderer.selfStartsWith.and.returnValue(true);
|
642
|
+
highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('Series 1');
|
643
|
+
|
644
|
+
opts = { chartOptions: {} };
|
645
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
|
646
|
+
let result = fn.call(funcContext);
|
647
|
+
|
648
|
+
expect(highchartsRenderer.selfStartsWith).toHaveBeenCalledWith('Series 1', 'Series ');
|
649
|
+
expect(result).toBe('500');
|
650
|
+
});
|
651
|
+
|
652
|
+
it('should use point name for columns when cols is null in drill-down pie', () => {
|
653
|
+
highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
|
654
|
+
funcContext.point.name = 'DrillDownPoint';
|
655
|
+
|
656
|
+
opts = { chartOptions: {} };
|
657
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
|
658
|
+
let result = fn.call(funcContext);
|
659
|
+
|
660
|
+
expect(result).toBe('500');
|
661
|
+
});
|
662
|
+
|
663
|
+
it('should swap rows and cols for drill-down pie when series name does not start with "Series "', () => {
|
664
|
+
highchartsRenderer.selfStartsWith.and.returnValue(false);
|
665
|
+
highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('CustomSeries');
|
666
|
+
|
667
|
+
opts = { chartOptions: {} };
|
668
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts, true);
|
669
|
+
let result = fn.call(funcContext);
|
670
|
+
|
671
|
+
expect(result).toBe('500');
|
672
|
+
});
|
673
|
+
});
|
674
|
+
|
675
|
+
describe('Delta column handling', () => {
|
676
|
+
it('should replace variant name when delta column field is series', () => {
|
677
|
+
opts = {
|
678
|
+
chartOptions: {
|
679
|
+
label: { show_value: true },
|
680
|
+
delta_column: {
|
681
|
+
field: 'series',
|
682
|
+
name: 'test_variant'
|
683
|
+
}
|
684
|
+
}
|
685
|
+
};
|
686
|
+
|
687
|
+
highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('test_variant' + highchartsRenderer.delimer + 'other');
|
688
|
+
|
689
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
690
|
+
let result = fn.call(funcContext);
|
691
|
+
|
692
|
+
expect(result).toBe('12,345.678');
|
693
|
+
});
|
694
|
+
|
695
|
+
it('should handle variant name matching when series name differs by underscores from delta column name', () => {
|
696
|
+
opts = {
|
697
|
+
chartOptions: {
|
698
|
+
label: { show_value: true },
|
699
|
+
delta_column: {
|
700
|
+
field: 'series',
|
701
|
+
name: 'test_variant'
|
702
|
+
}
|
703
|
+
}
|
704
|
+
};
|
705
|
+
|
706
|
+
highchartsRenderer.getSeriesNameInFormatterContext.and.returnValue('testvariant' + highchartsRenderer.delimer + 'other');
|
707
|
+
|
708
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
709
|
+
let result = fn.call(funcContext);
|
710
|
+
|
711
|
+
expect(result).toBe('12,345.678');
|
712
|
+
});
|
713
|
+
});
|
714
|
+
|
715
|
+
describe('Label options handling', () => {
|
716
|
+
it('should return raw value when show_out_of_x_axis is true but percentage logic is not triggered', () => {
|
717
|
+
opts = {
|
718
|
+
chartOptions: {
|
719
|
+
label: {
|
720
|
+
show_value: true,
|
721
|
+
show_out_of_x_axis: true
|
722
|
+
}
|
723
|
+
}
|
724
|
+
};
|
725
|
+
|
726
|
+
funcContext.y = 250;
|
727
|
+
mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
|
728
|
+
|
729
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
730
|
+
let result = fn.call(funcContext);
|
731
|
+
|
732
|
+
expect(result).toBe('250');
|
733
|
+
});
|
734
|
+
|
735
|
+
it('should return raw value when show_out_of_data_series is true but percentage logic is not triggered', () => {
|
736
|
+
opts = {
|
737
|
+
chartOptions: {
|
738
|
+
label: {
|
739
|
+
show_value: true,
|
740
|
+
show_out_of_data_series: true
|
741
|
+
}
|
742
|
+
}
|
743
|
+
};
|
744
|
+
|
745
|
+
funcContext.y = 200;
|
746
|
+
mockPivotData.getAggregator = jest.fn(() => ({ value: () => 800 }));
|
747
|
+
|
748
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
749
|
+
let result = fn.call(funcContext);
|
750
|
+
|
751
|
+
expect(result).toBe('200');
|
752
|
+
});
|
753
|
+
|
754
|
+
it('should return raw value when both percentage options are enabled but percentage logic is not triggered', () => {
|
755
|
+
opts = {
|
756
|
+
chartOptions: {
|
757
|
+
label: {
|
758
|
+
show_value: true,
|
759
|
+
show_out_of_x_axis: true,
|
760
|
+
show_out_of_data_series: true
|
761
|
+
}
|
762
|
+
}
|
763
|
+
};
|
764
|
+
|
765
|
+
funcContext.y = 250;
|
766
|
+
let callCount = 0;
|
767
|
+
mockPivotData.getAggregator = jest.fn(() => {
|
768
|
+
callCount++;
|
769
|
+
return { value: () => callCount === 1 ? 1000 : 800 };
|
770
|
+
});
|
771
|
+
|
772
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
773
|
+
let result = fn.call(funcContext);
|
774
|
+
|
775
|
+
expect(result).toBe('250');
|
776
|
+
});
|
777
|
+
|
778
|
+
it('should show only percentages without value when show_value is false but percentages are enabled', () => {
|
779
|
+
opts = {
|
780
|
+
chartOptions: {
|
781
|
+
label: {
|
782
|
+
show_value: false,
|
783
|
+
show_out_of_x_axis: true
|
784
|
+
}
|
785
|
+
}
|
786
|
+
};
|
787
|
+
|
788
|
+
funcContext.y = 250;
|
789
|
+
mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
|
790
|
+
|
791
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
792
|
+
let result = fn.call(funcContext);
|
793
|
+
|
794
|
+
expect(result).toBe('(25%)');
|
795
|
+
});
|
796
|
+
|
797
|
+
it('should not add percentage when value is falsy', () => {
|
798
|
+
opts = {
|
799
|
+
chartOptions: {
|
800
|
+
label: {
|
801
|
+
show_value: true,
|
802
|
+
show_out_of_x_axis: true
|
803
|
+
}
|
804
|
+
}
|
805
|
+
};
|
806
|
+
|
807
|
+
funcContext.y = 0;
|
808
|
+
mockPivotData.getAggregator = jest.fn(() => ({ value: () => 1000 }));
|
809
|
+
|
810
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
811
|
+
let result = fn.call(funcContext);
|
812
|
+
|
813
|
+
expect(result).toBe('0');
|
814
|
+
});
|
815
|
+
|
816
|
+
it('should not add percentage when axisTotal is falsy', () => {
|
817
|
+
opts = {
|
818
|
+
chartOptions: {
|
819
|
+
label: {
|
820
|
+
show_value: true,
|
821
|
+
show_out_of_x_axis: true
|
822
|
+
}
|
823
|
+
}
|
824
|
+
};
|
825
|
+
|
826
|
+
funcContext.y = 250;
|
827
|
+
mockPivotData.getAggregator = jest.fn(() => ({ value: () => 0 }));
|
828
|
+
|
829
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
830
|
+
let result = fn.call(funcContext);
|
831
|
+
|
832
|
+
expect(result).toBe('250');
|
833
|
+
});
|
834
|
+
});
|
835
|
+
|
836
|
+
describe('Object column handling', () => {
|
837
|
+
it('should extract name property from object columns', () => {
|
838
|
+
highchartsRenderer.getColsInFormatterContext.and.returnValue({ name: 'ObjectColumn' });
|
839
|
+
|
840
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
841
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
842
|
+
let result = fn.call(funcContext);
|
843
|
+
|
844
|
+
expect(result).toBe('12,345.678');
|
845
|
+
});
|
846
|
+
});
|
847
|
+
|
848
|
+
describe('Column initialization handling', () => {
|
849
|
+
it('should initialize cols to empty array when cols is falsy after array check', () => {
|
850
|
+
spyOn(lodash, 'isArray').and.returnValue(true);
|
851
|
+
highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
|
852
|
+
|
853
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
854
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
855
|
+
let result = fn.call(funcContext);
|
856
|
+
|
857
|
+
expect(result).toBe('12,345.678');
|
858
|
+
expect(lodash.isArray).toHaveBeenCalledWith(null);
|
859
|
+
});
|
860
|
+
});
|
861
|
+
|
862
|
+
describe('Waterfall breakdown handling', () => {
|
863
|
+
beforeEach(() => {
|
864
|
+
funcContext.series.options.className = 'waterfallBreakdown';
|
865
|
+
});
|
866
|
+
|
867
|
+
it('should transform rows and cols for waterfall breakdown', () => {
|
868
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
869
|
+
|
870
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
871
|
+
let result = fn.call(funcContext);
|
872
|
+
|
873
|
+
expect(highchartsRenderer.transformRowsAndColsForBreakdown).toHaveBeenCalled();
|
874
|
+
expect(result).toBe('12,345.678');
|
875
|
+
});
|
876
|
+
|
877
|
+
it('should return raw value for waterfall breakdown when show_out_of_data_series is true but percentage logic is not triggered', () => {
|
878
|
+
opts = {
|
879
|
+
chartOptions: {
|
880
|
+
label: {
|
881
|
+
show_value: true,
|
882
|
+
show_out_of_data_series: true
|
883
|
+
}
|
884
|
+
}
|
885
|
+
};
|
886
|
+
|
887
|
+
funcContext.y = 300;
|
888
|
+
mockPivotData.getRowKeys = jest.fn(() => ([['row1'], ['row2']]));
|
889
|
+
|
890
|
+
let callCount = 0;
|
891
|
+
mockPivotData.getAggregator = jest.fn((rows, cols) => {
|
892
|
+
if (rows.length === 1) return { value: () => 400 };
|
893
|
+
if (rows.length === 0 && cols.length > 0) return { value: () => 1200 };
|
894
|
+
return { value: () => 800 };
|
895
|
+
});
|
896
|
+
|
897
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
898
|
+
let result = fn.call(funcContext);
|
899
|
+
|
900
|
+
expect(result).toBe('300');
|
901
|
+
});
|
902
|
+
|
903
|
+
it('should calculate percentage using dataSeriesTotal when axisTotal is falsy (waterfall breakdown)', () => {
|
904
|
+
opts = {
|
905
|
+
chartOptions: {
|
906
|
+
label: {
|
907
|
+
show_value: false,
|
908
|
+
show_out_of_data_series: true
|
909
|
+
}
|
910
|
+
}
|
911
|
+
};
|
912
|
+
|
913
|
+
funcContext.y = 200; // value = 200
|
914
|
+
mockPivotData.getRowKeys = jest.fn(() => ([['row1'], ['row2']]));
|
915
|
+
|
916
|
+
let callCount = 0;
|
917
|
+
mockPivotData.getAggregator = jest.fn((rows, cols) => {
|
918
|
+
callCount++;
|
919
|
+
|
920
|
+
if (rows.length === 1) {
|
921
|
+
return { value: () => 400 };
|
922
|
+
}
|
923
|
+
if (rows.length === 0 && cols.length > 0) {
|
924
|
+
return { value: () => 0 };
|
925
|
+
}
|
926
|
+
|
927
|
+
return { value: () => 200 };
|
928
|
+
});
|
929
|
+
|
930
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
931
|
+
let result = fn.call(funcContext);
|
932
|
+
|
933
|
+
expect(result).toBe('(25%)');
|
934
|
+
});
|
935
|
+
|
936
|
+
it('should not add percentage when dataSeriesTotal is falsy (non-waterfall breakdown)', () => {
|
937
|
+
funcContext.series.options.className = 'regularSeries';
|
938
|
+
|
939
|
+
opts = {
|
940
|
+
chartOptions: {
|
941
|
+
label: {
|
942
|
+
show_value: false,
|
943
|
+
show_out_of_data_series: true
|
944
|
+
}
|
945
|
+
}
|
946
|
+
};
|
947
|
+
|
948
|
+
funcContext.y = 200;
|
949
|
+
|
950
|
+
mockPivotData.getAggregator = jest.fn((rows, cols) => {
|
951
|
+
if (cols.length === 0) {
|
952
|
+
return { value: () => 0 };
|
953
|
+
}
|
954
|
+
return { value: () => 200 };
|
955
|
+
});
|
956
|
+
|
957
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
958
|
+
let result = fn.call(funcContext);
|
959
|
+
|
960
|
+
expect(result).toBe('');
|
961
|
+
});
|
962
|
+
});
|
963
|
+
|
964
|
+
describe('Error handling', () => {
|
965
|
+
it('should fallback to basic formatting when aggregator throws error', () => {
|
966
|
+
mockPivotData.getAggregator = jest.fn(() => { throw new Error('Test error'); });
|
967
|
+
|
968
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
969
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
970
|
+
let result = fn.call(funcContext);
|
971
|
+
|
972
|
+
expect(result).toBe('12,345.678');
|
973
|
+
});
|
974
|
+
|
975
|
+
it('should handle null columns gracefully', () => {
|
976
|
+
highchartsRenderer.getColsInFormatterContext.and.returnValue(null);
|
977
|
+
|
978
|
+
opts = { chartOptions: { label: { show_value: true } } };
|
979
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
980
|
+
let result = fn.call(funcContext);
|
981
|
+
|
982
|
+
expect(result).toBe('12,345.678');
|
983
|
+
});
|
984
|
+
|
985
|
+
it('should handle empty options gracefully for drill-down pie', () => {
|
986
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, {}, true);
|
987
|
+
let result = fn.call(funcContext);
|
988
|
+
|
989
|
+
expect(result).toBe('12,345.678');
|
990
|
+
});
|
991
|
+
});
|
992
|
+
|
993
|
+
describe('Unit abbreviation options (fallback behavior)', () => {
|
994
|
+
it('should fallback to raw value formatting when useUnitAbbreviation is false and getFormattedNumber returns abbreviated value', () => {
|
995
|
+
opts = {
|
996
|
+
chartOptions: {
|
997
|
+
label: {
|
998
|
+
show_value: true,
|
999
|
+
useUnitAbbreviation: false
|
1000
|
+
}
|
1001
|
+
}
|
1002
|
+
};
|
1003
|
+
|
1004
|
+
jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('12.5K');
|
1005
|
+
|
1006
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
1007
|
+
let result = fn.call(funcContext);
|
1008
|
+
|
1009
|
+
expect(result).toBe('12,345.678');
|
1010
|
+
});
|
1011
|
+
|
1012
|
+
it('should fallback to raw value formatting when useUnitAbbreviation is true and getFormattedNumber returns abbreviated value', () => {
|
1013
|
+
opts = {
|
1014
|
+
chartOptions: {
|
1015
|
+
label: {
|
1016
|
+
show_value: true,
|
1017
|
+
useUnitAbbreviation: true
|
1018
|
+
}
|
1019
|
+
}
|
1020
|
+
};
|
1021
|
+
|
1022
|
+
jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('12.5K');
|
1023
|
+
|
1024
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
1025
|
+
let result = fn.call(funcContext);
|
1026
|
+
|
1027
|
+
expect(result).toBe('12,345.678');
|
1028
|
+
});
|
1029
|
+
|
1030
|
+
it('should fallback to raw value formatting when getFormattedNumber returns M-abbreviated value regardless of useUnitAbbreviation setting', () => {
|
1031
|
+
opts = {
|
1032
|
+
chartOptions: {
|
1033
|
+
label: {
|
1034
|
+
show_value: true,
|
1035
|
+
useUnitAbbreviation: false
|
1036
|
+
}
|
1037
|
+
}
|
1038
|
+
};
|
1039
|
+
|
1040
|
+
jest.spyOn(global.$.pivotUtilities, 'getFormattedNumber').mockReturnValue('1.2M');
|
1041
|
+
|
1042
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
1043
|
+
let result = fn.call(funcContext);
|
1044
|
+
|
1045
|
+
expect(result).toBe('12,345.678');
|
1046
|
+
});
|
1047
|
+
});
|
1048
|
+
|
1049
|
+
describe('Others name handling', () => {
|
1050
|
+
it('should handle others name replacement', () => {
|
1051
|
+
opts = {
|
1052
|
+
total_value_options: { some: 'option' },
|
1053
|
+
chartOptions: { label: { show_value: true } }
|
1054
|
+
};
|
1055
|
+
|
1056
|
+
highchartsRenderer.getOthersName.and.returnValue('CustomOthers');
|
1057
|
+
highchartsRenderer.getDrOthersInAxisState.and.returnValue({ cols: true });
|
1058
|
+
|
1059
|
+
let fn = highchartsRenderer.defaultDataLabelFormatter(mockPivotData, opts);
|
1060
|
+
let result = fn.call(funcContext);
|
1061
|
+
|
1062
|
+
expect(highchartsRenderer.getOthersName).toHaveBeenCalledWith(opts);
|
1063
|
+
expect(highchartsRenderer.getDrOthersInAxisState).toHaveBeenCalledWith(mockPivotData, 'CustomOthers');
|
1064
|
+
expect(highchartsRenderer.replaceDrOthersKeys).toHaveBeenCalled();
|
1065
|
+
expect(result).toBe('12,345.678');
|
1066
|
+
});
|
527
1067
|
});
|
528
1068
|
});
|
529
1069
|
|
@@ -7648,36 +8188,6 @@ describe('highcharts_renderer', () => {
|
|
7648
8188
|
);
|
7649
8189
|
});
|
7650
8190
|
|
7651
|
-
it('should return no data result if series is empty', () => {
|
7652
|
-
const chartOptions = {
|
7653
|
-
chart: {},
|
7654
|
-
series: [{ data: [] }, { data: [] }, {}],
|
7655
|
-
};
|
7656
|
-
const options = {};
|
7657
|
-
const noDataFnSpy = jest.spyOn(highchartsRenderer, 'getNoDataResult').mockImplementation(() => {});
|
7658
|
-
|
7659
|
-
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
7660
|
-
|
7661
|
-
expect(noDataFnSpy).toHaveBeenCalled();
|
7662
|
-
expect(options.error_has_occurred).toBeTruthy();
|
7663
|
-
expect(options.error_params).toBe(highchartsRenderer.widgetPlaceholders.nodata);
|
7664
|
-
});
|
7665
|
-
|
7666
|
-
it('should return too much data result if series is too long', () => {
|
7667
|
-
const chartOptions = {
|
7668
|
-
chart: {},
|
7669
|
-
series: [{ data: new Array(1000) }, { data: new Array(1000) }, {}],
|
7670
|
-
};
|
7671
|
-
const options = {};
|
7672
|
-
const noDataFnSpy = jest.spyOn(highchartsRenderer, 'getNoDataResult').mockImplementation(() => {});
|
7673
|
-
|
7674
|
-
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
7675
|
-
|
7676
|
-
expect(noDataFnSpy).toHaveBeenCalled();
|
7677
|
-
expect(options.error_has_occurred).toBeTruthy();
|
7678
|
-
expect(options.error_params).toBe(highchartsRenderer.widgetPlaceholders.tooMuchData);
|
7679
|
-
});
|
7680
|
-
|
7681
8191
|
it('should set hcInstance on options with chart object for graph table renderer to use', () => {
|
7682
8192
|
jest.useFakeTimers();
|
7683
8193
|
jest.spyOn(Highcharts, 'chart').mockImplementation(() => ({ chart: true }));
|
@@ -7695,40 +8205,330 @@ describe('highcharts_renderer', () => {
|
|
7695
8205
|
});
|
7696
8206
|
});
|
7697
8207
|
|
7698
|
-
describe('
|
7699
|
-
const
|
8208
|
+
describe('Error Throwing Functionality', () => {
|
8209
|
+
const {
|
8210
|
+
NoDataError,
|
8211
|
+
TooMuchDataError,
|
8212
|
+
DataConflictError,
|
8213
|
+
GaugeConfigurationError,
|
8214
|
+
BaseRendererError,
|
8215
|
+
GenericRenderingError,
|
8216
|
+
GenericComputationalError
|
8217
|
+
} = require('../src/errors');
|
7700
8218
|
|
7701
|
-
|
7702
|
-
|
7703
|
-
const expected = container.clone().html(highchartsRenderer.getWidgetPlaceholder(placeholderMeta));
|
7704
|
-
expect(highchartsRenderer.getNoDataResult()).toEqual(expected);
|
8219
|
+
beforeEach(() => {
|
8220
|
+
jest.clearAllMocks();
|
7705
8221
|
});
|
7706
8222
|
|
7707
|
-
|
7708
|
-
|
7709
|
-
const expected = container.clone().html(highchartsRenderer.getWidgetPlaceholder(placeholderMeta));
|
7710
|
-
expect(highchartsRenderer.getNoDataResult(true)).toEqual(expected);
|
8223
|
+
afterAll(() => {
|
8224
|
+
jest.restoreAllMocks();
|
7711
8225
|
});
|
7712
8226
|
|
7713
|
-
|
8227
|
+
describe('ptCreateElementAndDraw - Error Throwing', () => {
|
8228
|
+
it('should throw NoDataError when series is empty and not onlyText', () => {
|
8229
|
+
const chartOptions = {
|
8230
|
+
chart: {},
|
8231
|
+
series: [{ data: [] }, { data: [] }, {}],
|
8232
|
+
};
|
8233
|
+
const options = {};
|
7714
8234
|
|
7715
|
-
|
7716
|
-
|
7717
|
-
|
7718
|
-
|
8235
|
+
expect(() => {
|
8236
|
+
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
8237
|
+
}).toThrow(NoDataError);
|
8238
|
+
});
|
7719
8239
|
|
7720
|
-
|
7721
|
-
|
7722
|
-
|
7723
|
-
|
8240
|
+
it('should throw TooMuchDataError when series has too much data', () => {
|
8241
|
+
const chartOptions = {
|
8242
|
+
chart: {},
|
8243
|
+
series: [{ data: new Array(1000) }, { data: new Array(1000) }, {}],
|
8244
|
+
};
|
8245
|
+
const options = {};
|
8246
|
+
|
8247
|
+
expect(() => {
|
8248
|
+
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
8249
|
+
}).toThrow(TooMuchDataError);
|
8250
|
+
});
|
8251
|
+
|
8252
|
+
it('should not throw errors when onlyText is true even if no data', () => {
|
8253
|
+
const chartOptions = {
|
8254
|
+
chart: {},
|
8255
|
+
series: [{ data: [] }],
|
8256
|
+
onlyText: true,
|
8257
|
+
};
|
8258
|
+
const options = {};
|
8259
|
+
|
8260
|
+
expect(() => {
|
8261
|
+
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
8262
|
+
}).not.toThrow();
|
8263
|
+
});
|
8264
|
+
|
8265
|
+
it('should process normally when series has valid data', () => {
|
8266
|
+
jest.spyOn(Highcharts, 'chart').mockImplementation(() => ({ chart: true }));
|
8267
|
+
jest.useFakeTimers();
|
8268
|
+
|
8269
|
+
const chartOptions = {
|
8270
|
+
chart: {},
|
8271
|
+
series: [{ data: [1, 2, 3] }],
|
8272
|
+
};
|
8273
|
+
const options = {};
|
8274
|
+
|
8275
|
+
expect(() => {
|
8276
|
+
highchartsRenderer.ptCreateElementAndDraw(chartOptions, options);
|
8277
|
+
}).not.toThrow();
|
8278
|
+
|
8279
|
+
jest.runAllTimers();
|
8280
|
+
jest.restoreAllMocks();
|
8281
|
+
jest.useRealTimers();
|
8282
|
+
});
|
8283
|
+
});
|
8284
|
+
|
8285
|
+
describe('Data Conflict Error Throwing', () => {
|
8286
|
+
it('should throw DataConflictError when categories are below minimum in waterfall chart', () => {
|
8287
|
+
const options = {
|
8288
|
+
isBreakdown: true,
|
8289
|
+
uniqueCategories: ['A', 'B'], // Only 2 categories
|
8290
|
+
minCategories: 5,
|
8291
|
+
maxCategories: 10
|
8292
|
+
};
|
8293
|
+
|
8294
|
+
expect(() => {
|
8295
|
+
throw new DataConflictError({
|
8296
|
+
isBreakdown: options.isBreakdown,
|
8297
|
+
uniqueCategories: options.uniqueCategories,
|
8298
|
+
minCategories: options.minCategories,
|
8299
|
+
maxCategories: options.maxCategories
|
8300
|
+
});
|
8301
|
+
}).toThrow(DataConflictError);
|
8302
|
+
});
|
8303
|
+
|
8304
|
+
it('should throw DataConflictError when categories exceed maximum in waterfall chart', () => {
|
8305
|
+
const uniqueCategories = new Array(15).fill(0).map((_, i) => `Category${i}`); // 15 categories
|
8306
|
+
const options = {
|
8307
|
+
isBreakdown: false,
|
8308
|
+
uniqueCategories,
|
8309
|
+
minCategories: 3,
|
8310
|
+
maxCategories: 10
|
8311
|
+
};
|
8312
|
+
|
8313
|
+
expect(() => {
|
8314
|
+
throw new DataConflictError({
|
8315
|
+
isBreakdown: options.isBreakdown,
|
8316
|
+
uniqueCategories: options.uniqueCategories,
|
8317
|
+
minCategories: options.minCategories,
|
8318
|
+
maxCategories: options.maxCategories
|
8319
|
+
});
|
8320
|
+
}).toThrow(DataConflictError);
|
8321
|
+
});
|
8322
|
+
|
8323
|
+
it('should create DataConflictError with correct options for breakdown scenario', () => {
|
8324
|
+
const options = {
|
8325
|
+
isBreakdown: true,
|
8326
|
+
uniqueCategories: ['A'],
|
8327
|
+
minCategories: 5,
|
8328
|
+
maxCategories: 10
|
8329
|
+
};
|
8330
|
+
|
8331
|
+
try {
|
8332
|
+
throw new DataConflictError(options);
|
8333
|
+
} catch (error) {
|
8334
|
+
expect(error).toBeInstanceOf(DataConflictError);
|
8335
|
+
expect(error.code).toBe(5);
|
8336
|
+
expect(error.title).toBe('Data Conflict');
|
8337
|
+
expect(error.options).toEqual(options);
|
8338
|
+
expect(error.options.isBreakdown).toBe(true);
|
8339
|
+
expect(error.options.uniqueCategories).toEqual(['A']);
|
8340
|
+
expect(error.options.minCategories).toBe(5);
|
8341
|
+
expect(error.options.maxCategories).toBe(10);
|
8342
|
+
}
|
8343
|
+
});
|
8344
|
+
|
8345
|
+
it('should create DataConflictError with correct options for walkthrough scenario', () => {
|
8346
|
+
const options = {
|
8347
|
+
isBreakdown: false,
|
8348
|
+
uniqueCategories: ['A', 'B', 'C'],
|
8349
|
+
minCategories: 5,
|
8350
|
+
maxCategories: 8
|
8351
|
+
};
|
8352
|
+
|
8353
|
+
try {
|
8354
|
+
throw new DataConflictError(options);
|
8355
|
+
} catch (error) {
|
8356
|
+
expect(error).toBeInstanceOf(DataConflictError);
|
8357
|
+
expect(error.code).toBe(5);
|
8358
|
+
expect(error.title).toBe('Data Conflict');
|
8359
|
+
expect(error.options).toEqual(options);
|
8360
|
+
expect(error.options.isBreakdown).toBe(false);
|
8361
|
+
}
|
8362
|
+
});
|
7724
8363
|
});
|
7725
8364
|
|
7726
|
-
|
7727
|
-
|
7728
|
-
|
7729
|
-
|
7730
|
-
|
7731
|
-
|
8365
|
+
describe('Gauge Configuration Error Throwing', () => {
|
8366
|
+
it('should create GaugeConfigurationError with correct properties', () => {
|
8367
|
+
try {
|
8368
|
+
throw new GaugeConfigurationError();
|
8369
|
+
} catch (error) {
|
8370
|
+
expect(error).toBeInstanceOf(GaugeConfigurationError);
|
8371
|
+
expect(error.code).toBe(6);
|
8372
|
+
expect(error.title).toBe('Please configure goal and needle');
|
8373
|
+
expect(error.options).toEqual({});
|
8374
|
+
}
|
8375
|
+
});
|
8376
|
+
|
8377
|
+
it('should be instance of BaseRendererError', () => {
|
8378
|
+
const error = new GaugeConfigurationError();
|
8379
|
+
expect(error).toBeInstanceOf(require('../src/errors').BaseRendererError);
|
8380
|
+
});
|
8381
|
+
});
|
8382
|
+
|
8383
|
+
describe('Error handling in rhPivotView functions (testing private _handleComputationalError and _handleRenderingError)', () => {
|
8384
|
+
beforeEach(() => {
|
8385
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
8386
|
+
});
|
8387
|
+
|
8388
|
+
afterEach(() => {
|
8389
|
+
jest.restoreAllMocks();
|
8390
|
+
});
|
8391
|
+
|
8392
|
+
describe('Natural error conditions (testing private error handlers indirectly)', () => {
|
8393
|
+
it('should handle too much data error correctly', () => {
|
8394
|
+
// Create dataset that exceeds MAX_ROWS_FOR_SHOW_RESULTS to trigger TooMuchDataError
|
8395
|
+
const largeRowData = new Array(highchartsRenderer.MAX_ROWS_FOR_SHOW_RESULTS + 1)
|
8396
|
+
.fill({ field1: 'value1', field2: 'value2' });
|
8397
|
+
|
8398
|
+
const options = {
|
8399
|
+
onlyOptions: false,
|
8400
|
+
rendererOptions: {},
|
8401
|
+
renderer: jest.fn()
|
8402
|
+
};
|
8403
|
+
|
8404
|
+
expect(() => {
|
8405
|
+
highchartsRenderer.rhPivotView(largeRowData, options);
|
8406
|
+
}).toThrow(TooMuchDataError);
|
8407
|
+
});
|
8408
|
+
|
8409
|
+
it('should return empty object for too much data when onlyOptions is true', () => {
|
8410
|
+
// Create dataset that exceeds MAX_ROWS_FOR_SHOW_RESULTS
|
8411
|
+
const largeRowData = new Array(highchartsRenderer.MAX_ROWS_FOR_SHOW_RESULTS + 1)
|
8412
|
+
.fill({ field1: 'value1', field2: 'value2' });
|
8413
|
+
|
8414
|
+
const options = {
|
8415
|
+
onlyOptions: true,
|
8416
|
+
rendererOptions: {},
|
8417
|
+
renderer: jest.fn()
|
8418
|
+
};
|
8419
|
+
|
8420
|
+
const result = highchartsRenderer.rhPivotView(largeRowData, options);
|
8421
|
+
expect(result).toEqual({});
|
8422
|
+
});
|
8423
|
+
|
8424
|
+
it('should handle no data error correctly', () => {
|
8425
|
+
const options = {
|
8426
|
+
onlyOptions: false,
|
8427
|
+
rendererOptions: {},
|
8428
|
+
renderer: jest.fn()
|
8429
|
+
};
|
8430
|
+
|
8431
|
+
// Empty rowData should trigger NoDataError
|
8432
|
+
expect(() => {
|
8433
|
+
highchartsRenderer.rhPivotView([], options);
|
8434
|
+
}).toThrow(NoDataError);
|
8435
|
+
});
|
8436
|
+
|
8437
|
+
it('should return empty object for no data when onlyOptions is true', () => {
|
8438
|
+
const options = {
|
8439
|
+
onlyOptions: true,
|
8440
|
+
rendererOptions: {},
|
8441
|
+
renderer: jest.fn()
|
8442
|
+
};
|
8443
|
+
|
8444
|
+
const result = highchartsRenderer.rhPivotView([], options);
|
8445
|
+
expect(result).toEqual({});
|
8446
|
+
});
|
8447
|
+
|
8448
|
+
it('should handle renderer errors correctly', () => {
|
8449
|
+
const rowData = [{ field1: 'value1', field2: 'value2' }];
|
8450
|
+
const genericError = new Error('Renderer failed');
|
8451
|
+
|
8452
|
+
const options = {
|
8453
|
+
onlyOptions: false,
|
8454
|
+
rendererOptions: {},
|
8455
|
+
renderer: jest.fn().mockImplementation(() => {
|
8456
|
+
throw genericError;
|
8457
|
+
})
|
8458
|
+
};
|
8459
|
+
|
8460
|
+
// Generic errors from renderer should be wrapped in GenericRenderingError
|
8461
|
+
expect(() => {
|
8462
|
+
highchartsRenderer.rhPivotView(rowData, options);
|
8463
|
+
}).toThrow(GenericRenderingError);
|
8464
|
+
});
|
8465
|
+
|
8466
|
+
it('should return empty object for renderer errors when onlyOptions is true', () => {
|
8467
|
+
const rowData = [{ field1: 'value1', field2: 'value2' }];
|
8468
|
+
const genericError = new Error('Renderer failed');
|
8469
|
+
|
8470
|
+
const options = {
|
8471
|
+
onlyOptions: true,
|
8472
|
+
rendererOptions: {},
|
8473
|
+
renderer: jest.fn().mockImplementation(() => {
|
8474
|
+
throw genericError;
|
8475
|
+
})
|
8476
|
+
};
|
8477
|
+
|
8478
|
+
const result = highchartsRenderer.rhPivotView(rowData, options);
|
8479
|
+
expect(result).toEqual({});
|
8480
|
+
});
|
8481
|
+
|
8482
|
+
it('should re-throw BaseRendererError instances from renderer unchanged', () => {
|
8483
|
+
const rowData = [{ field1: 'value1', field2: 'value2' }];
|
8484
|
+
const originalError = new NoDataError();
|
8485
|
+
|
8486
|
+
const options = {
|
8487
|
+
onlyOptions: false,
|
8488
|
+
rendererOptions: {},
|
8489
|
+
renderer: jest.fn().mockImplementation(() => {
|
8490
|
+
throw originalError;
|
8491
|
+
})
|
8492
|
+
};
|
8493
|
+
|
8494
|
+
// BaseRendererError instances should be re-thrown unchanged
|
8495
|
+
expect(() => {
|
8496
|
+
highchartsRenderer.rhPivotView(rowData, options);
|
8497
|
+
}).toThrow(NoDataError);
|
8498
|
+
|
8499
|
+
expect(() => {
|
8500
|
+
highchartsRenderer.rhPivotView(rowData, options);
|
8501
|
+
}).toThrow(originalError);
|
8502
|
+
});
|
8503
|
+
|
8504
|
+
it('should handle null/undefined from renderer as GenericRenderingError', () => {
|
8505
|
+
const rowData = [{ field1: 'value1', field2: 'value2' }];
|
8506
|
+
|
8507
|
+
// Test null
|
8508
|
+
const nullOptions = {
|
8509
|
+
onlyOptions: false,
|
8510
|
+
rendererOptions: {},
|
8511
|
+
renderer: jest.fn().mockImplementation(() => {
|
8512
|
+
throw null;
|
8513
|
+
})
|
8514
|
+
};
|
8515
|
+
|
8516
|
+
expect(() => {
|
8517
|
+
highchartsRenderer.rhPivotView(rowData, nullOptions);
|
8518
|
+
}).toThrow(GenericRenderingError);
|
8519
|
+
|
8520
|
+
const undefinedOptions = {
|
8521
|
+
onlyOptions: false,
|
8522
|
+
rendererOptions: {},
|
8523
|
+
renderer: jest.fn().mockImplementation(() => {
|
8524
|
+
throw undefined;
|
8525
|
+
})
|
8526
|
+
};
|
8527
|
+
|
8528
|
+
expect(() => {
|
8529
|
+
highchartsRenderer.rhPivotView(rowData, undefinedOptions);
|
8530
|
+
}).toThrow(GenericRenderingError);
|
8531
|
+
});
|
7732
8532
|
});
|
7733
8533
|
});
|
7734
8534
|
});
|