diff --git a/packages/server-shared/package.json b/packages/server-shared/package.json index 12432e822..5ed9014c0 100644 --- a/packages/server-shared/package.json +++ b/packages/server-shared/package.json @@ -8,7 +8,9 @@ "build": "tsc --build", "dev": "tsc --watch", "lint": "biome check", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "exports": { ".": { @@ -37,7 +39,11 @@ } }, "dependencies": { + "@buster/vitest-config": "workspace:*", "@buster/typescript-config": "workspace:*", "zod": "catalog:" + }, + "devDependencies": { + "vitest": "catalog:" } } diff --git a/packages/server-shared/src/metrics/charts/chartConfigProps.test.ts b/packages/server-shared/src/metrics/charts/chartConfigProps.test.ts new file mode 100644 index 000000000..f03813e7e --- /dev/null +++ b/packages/server-shared/src/metrics/charts/chartConfigProps.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, it } from 'vitest'; +import { + type ChartConfigProps, + ChartConfigPropsSchema, + DEFAULT_CHART_CONFIG, + DEFAULT_CHART_CONFIG_ENTRIES, +} from './chartConfigProps'; +import { DEFAULT_CHART_THEME } from './configColors'; + +describe('chartConfigProps', () => { + describe('DEFAULT_CHART_CONFIG', () => { + it('should have the correct default values', () => { + expect(DEFAULT_CHART_CONFIG).toBeDefined(); + expect(DEFAULT_CHART_CONFIG).toMatchObject({ + selectedChartType: 'table', + columnSettings: {}, + columnLabelFormats: {}, + colors: DEFAULT_CHART_THEME, + showLegend: null, + gridLines: true, + goalLines: [], + trendlines: [], + disableTooltip: false, + }); + }); + + it('should be a valid ChartConfigProps object', () => { + // This should not throw + expect(() => ChartConfigPropsSchema.parse(DEFAULT_CHART_CONFIG)).not.toThrow(); + }); + + it('should have the expected color palette', () => { + expect(DEFAULT_CHART_CONFIG.colors).toHaveLength(10); + expect(DEFAULT_CHART_CONFIG.colors).toEqual([ + '#B399FD', + '#FC8497', + '#FBBC30', + '#279EFF', + '#E83562', + '#41F8FF', + '#F3864F', + '#C82184', + '#31FCB4', + '#E83562', + ]); + }); + + it('should have empty objects for column configurations', () => { + expect(DEFAULT_CHART_CONFIG.columnSettings).toEqual({}); + expect(DEFAULT_CHART_CONFIG.columnLabelFormats).toEqual({}); + }); + + it('should have empty arrays for annotations', () => { + expect(DEFAULT_CHART_CONFIG.goalLines).toEqual([]); + expect(DEFAULT_CHART_CONFIG.trendlines).toEqual([]); + }); + + it('should have correct boolean defaults', () => { + expect(DEFAULT_CHART_CONFIG.gridLines).toBe(true); + expect(DEFAULT_CHART_CONFIG.disableTooltip).toBe(false); + }); + + it('should have null as default for showLegend', () => { + expect(DEFAULT_CHART_CONFIG.showLegend).toBeNull(); + }); + }); + + describe('DEFAULT_CHART_CONFIG_ENTRIES', () => { + it('should be an array of key-value pairs', () => { + expect(DEFAULT_CHART_CONFIG_ENTRIES).toBeDefined(); + expect(Array.isArray(DEFAULT_CHART_CONFIG_ENTRIES)).toBe(true); + }); + + it('should contain all keys from DEFAULT_CHART_CONFIG', () => { + const configKeys = Object.keys(DEFAULT_CHART_CONFIG); + const entriesKeys = DEFAULT_CHART_CONFIG_ENTRIES.map(([key]) => key); + + expect(entriesKeys).toHaveLength(configKeys.length); + expect(entriesKeys.sort()).toEqual(configKeys.sort()); + }); + + it('should have matching values with DEFAULT_CHART_CONFIG', () => { + for (const [key, value] of DEFAULT_CHART_CONFIG_ENTRIES) { + expect(DEFAULT_CHART_CONFIG[key as keyof typeof DEFAULT_CHART_CONFIG]).toEqual(value); + } + }); + + it('should be reconstructable to DEFAULT_CHART_CONFIG', () => { + const reconstructed = Object.fromEntries(DEFAULT_CHART_CONFIG_ENTRIES); + expect(reconstructed).toEqual(DEFAULT_CHART_CONFIG); + }); + + it('should contain specific expected entries', () => { + const entriesMap = new Map(DEFAULT_CHART_CONFIG_ENTRIES); + + expect(entriesMap.get('selectedChartType')).toBe('table'); + expect(entriesMap.get('colors')).toEqual(DEFAULT_CHART_THEME); + expect(entriesMap.get('gridLines')).toBe(true); + expect(entriesMap.get('showLegend')).toBeNull(); + expect(entriesMap.get('disableTooltip')).toBe(false); + }); + + it('should have the correct number of entries', () => { + // Count the number of properties we expect + const expectedKeys = [ + 'selectedChartType', + 'columnSettings', + 'columnLabelFormats', + 'colors', + 'showLegend', + 'gridLines', + 'goalLines', + 'trendlines', + 'disableTooltip', + // Plus any additional properties from the spread schemas + ]; + + // The actual count will depend on what's in the spread schemas + expect(DEFAULT_CHART_CONFIG_ENTRIES.length).toBeGreaterThanOrEqual(expectedKeys.length); + }); + }); + + describe('ChartConfigPropsSchema defaults', () => { + it('should create valid defaults when parsing empty object', () => { + const result = ChartConfigPropsSchema.parse({}); + + expect(result).toMatchObject({ + selectedChartType: 'table', + columnSettings: {}, + columnLabelFormats: {}, + colors: DEFAULT_CHART_THEME, + showLegend: null, + gridLines: true, + goalLines: [], + trendlines: [], + disableTooltip: false, + }); + }); + + it('should override defaults when values are provided', () => { + const customConfig = { + selectedChartType: 'bar' as const, + gridLines: false, + colors: ['#FF0000', '#00FF00'], + }; + + const result = ChartConfigPropsSchema.parse(customConfig); + + expect(result.selectedChartType).toBe('bar'); + expect(result.gridLines).toBe(false); + expect(result.colors).toEqual(['#FF0000', '#00FF00']); + // Other defaults should remain + expect(result.showLegend).toBeNull(); + expect(result.disableTooltip).toBe(false); + }); + + it('should handle partial configurations correctly', () => { + const partialConfig = { + showLegend: true, + disableTooltip: true, + }; + + const result = ChartConfigPropsSchema.parse(partialConfig); + + expect(result.showLegend).toBe(true); + expect(result.disableTooltip).toBe(true); + // Defaults should be applied for missing fields + expect(result.selectedChartType).toBe('table'); + expect(result.colors).toEqual(DEFAULT_CHART_THEME); + }); + + it('should handle complex column configurations', () => { + const configWithColumns = { + columnSettings: { + revenue: { showDataLabels: true, columnVisualization: 'line' as const }, + profit: { showDataLabels: false, barRoundness: 12 }, + }, + columnLabelFormats: { + revenue: { prefix: '$', suffix: 'M' }, + }, + }; + + const result = ChartConfigPropsSchema.parse(configWithColumns); + + // Check that our custom settings are preserved + expect(result.columnSettings.revenue).toMatchObject({ + showDataLabels: true, + columnVisualization: 'line', + }); + expect(result.columnSettings.profit).toMatchObject({ + showDataLabels: false, + barRoundness: 12, + }); + + // Check that defaults are applied to column settings + expect(result.columnSettings.revenue.lineWidth).toBe(2); + expect(result.columnSettings.profit.columnVisualization).toBe('bar'); + + expect(result.columnLabelFormats.revenue).toMatchObject({ + prefix: '$', + suffix: 'M', + }); + }); + }); + + describe('Type safety', () => { + it('should maintain type safety for DEFAULT_CHART_CONFIG', () => { + // This is a compile-time test - if it compiles, it passes + const config: ChartConfigProps = DEFAULT_CHART_CONFIG; + + // Test that we can access typed properties + const chartType: ChartConfigProps['selectedChartType'] = config.selectedChartType; + const colors: string[] = config.colors; + const showLegend: boolean | null = config.showLegend; + + expect(chartType).toBeDefined(); + expect(colors).toBeDefined(); + expect(showLegend).toBeDefined(); + }); + + it('should ensure DEFAULT_CHART_CONFIG_ENTRIES maintains proper typing', () => { + // Verify entries can be used in typed contexts + for (const [key, value] of DEFAULT_CHART_CONFIG_ENTRIES) { + expect(typeof key).toBe('string'); + expect(value).toBeDefined(); + } + + // Verify reconstruction maintains type + const reconstructed: Record = Object.fromEntries( + DEFAULT_CHART_CONFIG_ENTRIES + ); + expect(reconstructed).toBeDefined(); + }); + }); +}); diff --git a/packages/server-shared/vitest.config.ts b/packages/server-shared/vitest.config.ts new file mode 100644 index 000000000..d86b4007a --- /dev/null +++ b/packages/server-shared/vitest.config.ts @@ -0,0 +1,3 @@ +import { baseConfig } from '@buster/vitest-config'; + +export default baseConfig; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 592a1858b..d95039af1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -760,9 +760,16 @@ importers: '@buster/typescript-config': specifier: workspace:* version: link:../typescript-config + '@buster/vitest-config': + specifier: workspace:* + version: link:../vitest-config zod: specifier: 'catalog:' version: 3.25.67 + devDependencies: + vitest: + specifier: 'catalog:' + version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@20.19.2)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.2(@types/node@20.19.2)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) packages/slack: dependencies: