From a810e6261ad6f0ce5eb8e8d152f753faa96acc16 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Fri, 4 Apr 2025 10:07:16 -0600 Subject: [PATCH] tests for some common helpers --- .../charts/commonHelpers/axisHelper.test.ts | 498 ++++++++++++++++++ .../commonHelpers/pieLabelHelpers.test.ts | 69 +++ .../charts/commonHelpers/titleHelpers.test.ts | 58 ++ .../ui/charts/commonHelpers/titleHelpers.ts | 4 +- 4 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 web/src/components/ui/charts/commonHelpers/axisHelper.test.ts create mode 100644 web/src/components/ui/charts/commonHelpers/pieLabelHelpers.test.ts create mode 100644 web/src/components/ui/charts/commonHelpers/titleHelpers.test.ts diff --git a/web/src/components/ui/charts/commonHelpers/axisHelper.test.ts b/web/src/components/ui/charts/commonHelpers/axisHelper.test.ts new file mode 100644 index 000000000..af8f77f5a --- /dev/null +++ b/web/src/components/ui/charts/commonHelpers/axisHelper.test.ts @@ -0,0 +1,498 @@ +import { formatXAxisLabel, formatYAxisLabel, yAxisSimilar } from './axisHelper'; +import { formatLabel } from '@/lib/columnFormatter'; +import { formatChartLabelDelimiter } from './labelHelpers'; +import { ChartType } from '@/api/asset_interfaces/metric/charts'; +import type { SimplifiedColumnType, ColumnMetaData } from '@/api/asset_interfaces/metric'; + +// Mock dependencies +jest.mock('@/lib/columnFormatter', () => ({ + formatLabel: jest.fn() +})); + +jest.mock('./labelHelpers', () => ({ + formatChartLabelDelimiter: jest.fn() +})); + +describe('formatXAxisLabel', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should format a number value using formatLabel', () => { + // Setup + const value = 1000; + const selectedAxis = { x: ['revenue'], y: ['growth'] }; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + } + }; + const xAxisColumnMetadata: ColumnMetaData = { + name: 'revenue', + min_value: 0, + max_value: 5000, + unique_values: 100, + simple_type: 'number', + type: 'float' + }; + const selectedChartType = ChartType.Bar; + + // Execute + const result = formatXAxisLabel( + value, + selectedAxis, + columnLabelFormats, + xAxisColumnMetadata, + selectedChartType + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith(value, { + columnType: 'number', + style: 'currency', + currency: 'USD', + compactNumbers: true + }); + }); + + it('should use formatLabel for Scatter charts even with string values', () => { + // Setup + const value = 'some-string'; + const selectedAxis = { x: ['category'], y: ['count'] }; + const columnLabelFormats = { + category: { + columnType: 'text' as SimplifiedColumnType, + style: 'string' as const + } + }; + const xAxisColumnMetadata = undefined; + const selectedChartType = ChartType.Scatter; + + // Execute + formatXAxisLabel( + value, + selectedAxis, + columnLabelFormats, + xAxisColumnMetadata, + selectedChartType + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith(value, { + columnType: 'text', + style: 'string', + compactNumbers: false + }); + }); + + it('should not use compact numbers when range is small', () => { + // Setup + const value = 100; + const selectedAxis = { x: ['smallValue'], y: ['count'] }; + const columnLabelFormats = { + smallValue: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + } + }; + const xAxisColumnMetadata: ColumnMetaData = { + name: 'smallValue', + min_value: 0, + max_value: 500, + unique_values: 50, + simple_type: 'number', + type: 'float' + }; + const selectedChartType = ChartType.Line; + + // Execute + formatXAxisLabel( + value, + selectedAxis, + columnLabelFormats, + xAxisColumnMetadata, + selectedChartType + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith(value, { + columnType: 'number', + style: 'number', + compactNumbers: false + }); + }); + + it('should use compact numbers when range is large', () => { + // Setup + const value = 5000; + const selectedAxis = { x: ['largeValue'], y: ['count'] }; + const columnLabelFormats = { + largeValue: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + } + }; + const xAxisColumnMetadata: ColumnMetaData = { + name: 'largeValue', + min_value: 0, + max_value: 10000, + unique_values: 200, + simple_type: 'number', + type: 'float' + }; + const selectedChartType = ChartType.Bar; + + // Execute + formatXAxisLabel( + value, + selectedAxis, + columnLabelFormats, + xAxisColumnMetadata, + selectedChartType + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith(value, { + columnType: 'number', + style: 'number', + compactNumbers: true + }); + }); + + it('should use formatChartLabelDelimiter for string values in non-scatter charts', () => { + // Setup + const value = 'category-name'; + const selectedAxis = { x: ['category'], y: ['count'] }; + const columnLabelFormats = { + category: { + columnType: 'text' as SimplifiedColumnType, + style: 'string' as const + } + }; + const xAxisColumnMetadata = undefined; + const selectedChartType = ChartType.Bar; + + // Execute + formatXAxisLabel( + value, + selectedAxis, + columnLabelFormats, + xAxisColumnMetadata, + selectedChartType + ); + + // Verify + expect(formatChartLabelDelimiter).toHaveBeenCalledWith(value, columnLabelFormats); + }); +}); + +describe('formatYAxisLabel', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should use percentage formatting when usePercentageModeAxis is true', () => { + // Setup + const value = 0.75; + const axisColumnNames = ['growth', 'revenue']; + const canUseSameFormatter = true; + const columnLabelFormats = { + growth: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + }, + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + } + }; + const usePercentageModeAxis = true; + + // Execute + const result = formatYAxisLabel( + value, + axisColumnNames, + canUseSameFormatter, + columnLabelFormats, + usePercentageModeAxis + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith( + value, + { columnType: 'number', style: 'percent' }, + false + ); + }); + + it('should use the first column format when canUseSameFormatter is true', () => { + // Setup + const value = 1000; + const axisColumnNames = ['revenue', 'cost']; + const canUseSameFormatter = true; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + }, + cost: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'EUR' + } + }; + const usePercentageModeAxis = false; + const compactNumbers = true; + + // Execute + formatYAxisLabel( + value, + axisColumnNames, + canUseSameFormatter, + columnLabelFormats, + usePercentageModeAxis, + compactNumbers + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith( + value, + { + columnType: 'number', + style: 'currency', + currency: 'USD', + compactNumbers: true + }, + false + ); + }); + + it('should use generic number format when canUseSameFormatter is false', () => { + // Setup + const value = 1000; + const axisColumnNames = ['revenue', 'growth']; + const canUseSameFormatter = false; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + }, + growth: { + columnType: 'number' as SimplifiedColumnType, + style: 'percent' as const + } + }; + const usePercentageModeAxis = false; + const compactNumbers = true; + + // Execute + formatYAxisLabel( + value, + axisColumnNames, + canUseSameFormatter, + columnLabelFormats, + usePercentageModeAxis, + compactNumbers + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith( + value, + { + columnType: 'number', + style: 'number', + compactNumbers: true + }, + false + ); + }); + + it('should respect the compactNumbers parameter when false', () => { + // Setup + const value = 10000; + const axisColumnNames = ['count']; + const canUseSameFormatter = true; + const columnLabelFormats = { + count: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + } + }; + const usePercentageModeAxis = false; + const compactNumbers = false; + + // Execute + formatYAxisLabel( + value, + axisColumnNames, + canUseSameFormatter, + columnLabelFormats, + usePercentageModeAxis, + compactNumbers + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith( + value, + { + columnType: 'number', + style: 'number', + compactNumbers: false + }, + false + ); + }); + + it('should default compactNumbers to true if not provided', () => { + // Setup + const value = 10000; + const axisColumnNames = ['count']; + const canUseSameFormatter = true; + const columnLabelFormats = { + count: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + } + }; + const usePercentageModeAxis = false; + // No compactNumbers parameter provided + + // Execute + formatYAxisLabel( + value, + axisColumnNames, + canUseSameFormatter, + columnLabelFormats, + usePercentageModeAxis + ); + + // Verify + expect(formatLabel).toHaveBeenCalledWith( + value, + { + columnType: 'number', + style: 'number', + compactNumbers: true + }, + false + ); + }); +}); + +describe('yAxisSimilar', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return true when all y-axis variables have the same style and currency', () => { + // Setup + const yAxis = ['revenue', 'sales']; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + }, + sales: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + } + }; + + // Execute + const result = yAxisSimilar(yAxis, columnLabelFormats); + + // Verify + expect(result).toBe(true); + }); + + it('should return false when y-axis variables have different styles', () => { + // Setup + const yAxis = ['revenue', 'percentage']; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + }, + percentage: { + columnType: 'number' as SimplifiedColumnType, + style: 'percent' as const + } + }; + + // Execute + const result = yAxisSimilar(yAxis, columnLabelFormats); + + // Verify + expect(result).toBe(false); + }); + + it('should return false when y-axis variables have different currencies', () => { + // Setup + const yAxis = ['usd_revenue', 'eur_revenue']; + const columnLabelFormats = { + usd_revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + }, + eur_revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'EUR' + } + }; + + // Execute + const result = yAxisSimilar(yAxis, columnLabelFormats); + + // Verify + expect(result).toBe(false); + }); + + it('should return true for a single y-axis variable', () => { + // Setup + const yAxis = ['revenue']; + const columnLabelFormats = { + revenue: { + columnType: 'number' as SimplifiedColumnType, + style: 'currency' as const, + currency: 'USD' + } + }; + + // Execute + const result = yAxisSimilar(yAxis, columnLabelFormats); + + // Verify + expect(result).toBe(true); + }); + + it('should handle y-axis variables with missing format properties', () => { + // Setup + const yAxis = ['value1', 'value2']; + const columnLabelFormats = { + value1: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + }, + value2: { + columnType: 'number' as SimplifiedColumnType, + style: 'number' as const + } + }; + + // Execute + const result = yAxisSimilar(yAxis, columnLabelFormats); + + // Verify + expect(result).toBe(true); + }); +}); diff --git a/web/src/components/ui/charts/commonHelpers/pieLabelHelpers.test.ts b/web/src/components/ui/charts/commonHelpers/pieLabelHelpers.test.ts new file mode 100644 index 000000000..d6cdbe3c6 --- /dev/null +++ b/web/src/components/ui/charts/commonHelpers/pieLabelHelpers.test.ts @@ -0,0 +1,69 @@ +import { InnerLabelTitleRecord, getPieInnerLabelTitle } from './pieLabelHelpers'; +import type { BusterChartConfigProps } from '@/api/asset_interfaces/metric/charts'; + +describe('pieLabelHelpers', () => { + describe('InnerLabelTitleRecord', () => { + it('should have the correct titles for each aggregate type', () => { + expect(InnerLabelTitleRecord.sum).toBe('Total'); + expect(InnerLabelTitleRecord.average).toBe('Average'); + expect(InnerLabelTitleRecord.median).toBe('Median'); + expect(InnerLabelTitleRecord.max).toBe('Max'); + expect(InnerLabelTitleRecord.min).toBe('Min'); + expect(InnerLabelTitleRecord.count).toBe('Count'); + }); + }); + + describe('getPieInnerLabelTitle', () => { + it('should return the provided title when pieInnerLabelTitle is provided', () => { + const pieInnerLabelTitle = 'Custom Title'; + const pieInnerLabelAggregate: BusterChartConfigProps['pieInnerLabelAggregate'] = 'sum'; + + const result = getPieInnerLabelTitle(pieInnerLabelTitle, pieInnerLabelAggregate); + + expect(result).toBe('Custom Title'); + }); + + it('should return the aggregate title when pieInnerLabelTitle is not provided', () => { + const pieInnerLabelTitle = undefined; + const pieInnerLabelAggregate: BusterChartConfigProps['pieInnerLabelAggregate'] = 'average'; + + const result = getPieInnerLabelTitle(pieInnerLabelTitle, pieInnerLabelAggregate); + + expect(result).toBe('Average'); + }); + + it('should default to "sum" aggregate when pieInnerLabelAggregate is not provided', () => { + const pieInnerLabelTitle = undefined; + const pieInnerLabelAggregate = undefined; + + const result = getPieInnerLabelTitle(pieInnerLabelTitle, pieInnerLabelAggregate); + + expect(result).toBe('Total'); + }); + + it('should fall back to aggregate title when pieInnerLabelTitle is null', () => { + const pieInnerLabelTitle = null as unknown as BusterChartConfigProps['pieInnerLabelTitle']; + const pieInnerLabelAggregate: BusterChartConfigProps['pieInnerLabelAggregate'] = 'median'; + + const result = getPieInnerLabelTitle(pieInnerLabelTitle, pieInnerLabelAggregate); + + expect(result).toBe('Median'); + }); + + it('should work with each type of aggregate', () => { + const testCases: Array> = [ + 'sum', + 'average', + 'median', + 'max', + 'min', + 'count' + ]; + + testCases.forEach((aggregate) => { + const result = getPieInnerLabelTitle(undefined, aggregate); + expect(result).toBe(InnerLabelTitleRecord[aggregate]); + }); + }); + }); +}); diff --git a/web/src/components/ui/charts/commonHelpers/titleHelpers.test.ts b/web/src/components/ui/charts/commonHelpers/titleHelpers.test.ts new file mode 100644 index 000000000..9ee06531f --- /dev/null +++ b/web/src/components/ui/charts/commonHelpers/titleHelpers.test.ts @@ -0,0 +1,58 @@ +import { truncateWithEllipsis } from './titleHelpers'; + +describe('truncateWithEllipsis', () => { + it('should return the original text when shorter than maxLength', () => { + const shortText = 'Short text'; + expect(truncateWithEllipsis(shortText)).toBe(shortText); + }); + + it('should truncate text with ellipsis when longer than default maxLength', () => { + const longText = + 'This is a very long text that should be truncated because it exceeds the default max length of 52 characters'; + const result = truncateWithEllipsis(longText); + + // The result should be shorter than the original + expect(result.length).toBeLessThan(longText.length); + + // Should match the default max length (including the ellipsis) + expect(result.length).toBeLessThanOrEqual(52); + + // Should end with ellipsis + expect(result.endsWith('...')).toBe(true); + }); + + it('should truncate to the specified maxLength', () => { + const longText = 'This text should be truncated to 20 characters'; + const maxLength = 20; + const result = truncateWithEllipsis(longText, maxLength); + + // Should match the specified max length (including the ellipsis) + expect(result.length).toBeLessThanOrEqual(maxLength); + + // Should end with ellipsis + expect(result.endsWith('...')).toBe(true); + }); + + it('should handle edge case with maxLength less than 4', () => { + // Testing with very small maxLength value + // lodash/truncate handles the ellipsis calculations internally + const text = 'Some text'; + const maxLength = 3; + const result = truncateWithEllipsis(text, maxLength); + + expect(result.length).toBeLessThanOrEqual(maxLength); + }); + + it('should handle empty string', () => { + const emptyText = ''; + expect(truncateWithEllipsis(emptyText)).toBe(emptyText); + }); + + it('should handle strings exactly at the maxLength boundary', () => { + const text = 'a'.repeat(52); // Exactly the default maxLength + expect(truncateWithEllipsis(text)).toBe(text); + + const text2 = 'a'.repeat(53); // One over the default maxLength + expect(truncateWithEllipsis(text2).length).toBeLessThan(text2.length); + }); +}); diff --git a/web/src/components/ui/charts/commonHelpers/titleHelpers.ts b/web/src/components/ui/charts/commonHelpers/titleHelpers.ts index eafe33e5e..9a884ba77 100644 --- a/web/src/components/ui/charts/commonHelpers/titleHelpers.ts +++ b/web/src/components/ui/charts/commonHelpers/titleHelpers.ts @@ -1,2 +1,4 @@ +import truncate from 'lodash/truncate'; + export const truncateWithEllipsis = (text: string, maxLength: number = 52) => - text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text; + text.length > maxLength ? truncate(text, { length: maxLength }) : text;