/** * @fileoverview Unit tests for pivot table data transformation utilities * * This test suite comprehensively tests the pivot table utility functions, * covering various dimension scenarios, edge cases, and configuration validation. * * Test Coverage: * - transformToPivotTable function with 0-N dimensions * - Configuration validation and error handling * - Helper functions for row generation and calculation * - MAX_DIMENSIONS constraint validation * - Edge cases with empty data, null values, and malformed input */ import { transformToPivotTable, validatePivotTableConfig, generateRowId, createEmptyMetricValues, extractDimensionValues, extractMetricValues, isDataRow, isSubtotalRow, isTotalRow, groupDataByDimension, calculateSubtotals, calculateGrandTotals, createSubtotalRow, createGrandTotalRow, type PivotTableConfig, type DatabaseRow, type PivotTableRow, getNextSortState, } from "@/src/features/widgets/utils/pivot-table-utils"; describe("pivot-table-utils", () => { const MAX_PIVOT_TABLE_DIMENSIONS = 2; // Sample test data const sampleData: DatabaseRow[] = [ { model: "gpt-4", user: "alice", count: 10, avg_cost: 0.5 }, { model: "gpt-4", user: "bob", count: 15, avg_cost: 0.6 }, { model: "gpt-3.5", user: "alice", count: 20, avg_cost: 0.2 }, { model: "gpt-3.5", user: "bob", count: 25, avg_cost: 0.4 }, { model: "claude", user: "alice", count: 5, avg_cost: 0.8 }, ]; const sampleMetrics = ["count", "avg_cost"]; describe("validatePivotTableConfig", () => { it("should pass validation for valid configurations", () => { const validConfig: PivotTableConfig = { dimensions: ["model"], metrics: ["count"], rowLimit: 20, }; expect(() => validatePivotTableConfig(validConfig)).not.toThrow(); }); it("should pass validation for valid dimension counts", () => { const configWithNoDimensions: PivotTableConfig = { dimensions: [], metrics: ["count"], }; const configWithOneDimension: PivotTableConfig = { dimensions: ["dim1"], metrics: ["count"], }; const configWithTwoDimensions: PivotTableConfig = { dimensions: ["dim1", "dim2"], metrics: ["count"], }; expect(() => validatePivotTableConfig(configWithNoDimensions), ).not.toThrow(); expect(() => validatePivotTableConfig(configWithOneDimension), ).not.toThrow(); expect(() => validatePivotTableConfig(configWithTwoDimensions), ).not.toThrow(); }); it("should throw error for empty metrics", () => { const invalidConfig: PivotTableConfig = { dimensions: ["model"], metrics: [], }; expect(() => validatePivotTableConfig(invalidConfig)).toThrow( "At least one metric is required for pivot table", ); }); it("should throw error for invalid row limit", () => { const invalidConfig: PivotTableConfig = { dimensions: ["model"], metrics: ["count"], rowLimit: 0, }; expect(() => validatePivotTableConfig(invalidConfig)).toThrow( "Row limit must be a positive number", ); }); it("should throw error when dimension count exceeds maximum", () => { const tooManyDimensions = Array.from( { length: MAX_PIVOT_TABLE_DIMENSIONS + 1 }, (_, i) => `dim${i + 1}`, ); const invalidConfig: PivotTableConfig = { dimensions: tooManyDimensions, metrics: ["count"], }; expect(() => validatePivotTableConfig(invalidConfig)).toThrow( `Cannot create pivot table with ${MAX_PIVOT_TABLE_DIMENSIONS + 1} dimensions. ` + `Maximum supported dimensions: ${MAX_PIVOT_TABLE_DIMENSIONS}`, ); }); }); describe("generateRowId", () => { it("should generate consistent IDs for same inputs", () => { const dimensionValues = { model: "gpt-4", user: "alice" }; const id1 = generateRowId(dimensionValues, "data", 0); const id2 = generateRowId(dimensionValues, "data", 0); expect(id1).toBe(id2); expect(id1).toContain("data-0"); }); it("should generate different IDs for different inputs", () => { const dimensionValues1 = { model: "gpt-4" }; const dimensionValues2 = { model: "gpt-3.5" }; const id1 = generateRowId(dimensionValues1, "data", 0); const id2 = generateRowId(dimensionValues2, "data", 0); expect(id1).not.toBe(id2); }); it("should include type and level in ID", () => { const dimensionValues = { model: "gpt-4" }; const dataId = generateRowId(dimensionValues, "data", 0); const subtotalId = generateRowId(dimensionValues, "subtotal", 1); const totalId = generateRowId(dimensionValues, "total", 0); expect(dataId).toContain("data-0"); expect(subtotalId).toContain("subtotal-1"); expect(totalId).toContain("total-0"); }); }); describe("createEmptyMetricValues", () => { it("should create object with all metrics set to 0", () => { const metrics = ["count", "avg_cost", "total_tokens"]; const result = createEmptyMetricValues(metrics); expect(result).toEqual({ count: 0, avg_cost: 0, total_tokens: 0, }); }); it("should handle empty metrics array", () => { const result = createEmptyMetricValues([]); expect(result).toEqual({}); }); }); describe("extractDimensionValues", () => { it("should extract specified dimension values from row", () => { const row: DatabaseRow = { model: "gpt-4", user: "alice", count: 10, avg_cost: 0.5, }; const result = extractDimensionValues(row, ["model", "user"]); expect(result).toEqual({ model: "gpt-4", user: "alice", }); }); it("should handle missing dimension values", () => { const row: DatabaseRow = { model: "gpt-4", count: 10, }; const result = extractDimensionValues(row, ["model", "missing"]); expect(result).toEqual({ model: "gpt-4", missing: "", }); }); it("should return empty object for empty dimensions", () => { const row: DatabaseRow = { model: "gpt-4", count: 10 }; const result = extractDimensionValues(row, []); expect(result).toEqual({}); }); }); describe("extractMetricValues", () => { it("should extract specified metric values from row", () => { const row: DatabaseRow = { model: "gpt-4", count: 10, avg_cost: 0.5, total_tokens: 1000, }; const result = extractMetricValues(row, ["count", "avg_cost"]); expect(result).toEqual({ count: 10, avg_cost: 0.5, }); }); it("should handle missing metric values", () => { const row: DatabaseRow = { model: "gpt-4", count: 10, }; const result = extractMetricValues(row, ["count", "missing"]); expect(result).toEqual({ count: 10, missing: 0, }); }); it("should handle non-numeric values", () => { const row: DatabaseRow = { model: "gpt-4", count: "invalid", avg_cost: null, }; const result = extractMetricValues(row, ["count", "avg_cost"]); expect(result).toEqual({ count: 0, avg_cost: 0, }); }); }); describe("row type helpers", () => { const dataRow: PivotTableRow = { id: "test-data", type: "data", level: 0, label: "Test", values: { count: 10 }, }; const subtotalRow: PivotTableRow = { id: "test-subtotal", type: "subtotal", level: 0, label: "Subtotal", values: { count: 20 }, isSubtotal: true, }; const totalRow: PivotTableRow = { id: "test-total", type: "total", level: 0, label: "Total", values: { count: 30 }, isTotal: true, }; describe("isDataRow", () => { it("should correctly identify data rows", () => { expect(isDataRow(dataRow)).toBe(true); expect(isDataRow(subtotalRow)).toBe(false); expect(isDataRow(totalRow)).toBe(false); }); }); describe("isSubtotalRow", () => { it("should correctly identify subtotal rows", () => { expect(isSubtotalRow(dataRow)).toBe(false); expect(isSubtotalRow(subtotalRow)).toBe(true); expect(isSubtotalRow(totalRow)).toBe(false); }); }); describe("isTotalRow", () => { it("should correctly identify total rows", () => { expect(isTotalRow(dataRow)).toBe(false); expect(isTotalRow(subtotalRow)).toBe(false); expect(isTotalRow(totalRow)).toBe(true); }); }); }); describe("groupDataByDimension", () => { it("should group data by specified dimension", () => { const result = groupDataByDimension(sampleData, "model"); expect(Object.keys(result)).toEqual(["gpt-4", "gpt-3.5", "claude"]); expect(result["gpt-4"]).toHaveLength(2); expect(result["gpt-3.5"]).toHaveLength(2); expect(result["claude"]).toHaveLength(1); }); it("should handle missing dimension values", () => { const dataWithMissing: DatabaseRow[] = [ { model: "gpt-4", count: 10 }, { count: 15 }, // missing model { model: "gpt-3.5", count: 20 }, ]; const result = groupDataByDimension(dataWithMissing, "model"); expect(Object.keys(result).sort()).toEqual(["gpt-3.5", "gpt-4", "n/a"]); expect(result["n/a"]).toHaveLength(1); }); it("should handle empty data", () => { const result = groupDataByDimension([], "model"); expect(result).toEqual({}); }); }); describe("calculateSubtotals", () => { it("should calculate subtotals for numeric metrics", () => { const data: DatabaseRow[] = [ { count: 10, avg_cost: 0.5 }, { count: 15, avg_cost: 0.6 }, { count: 20, avg_cost: 0.4 }, ]; const result = calculateSubtotals(data, ["count", "avg_cost"]); expect(result).toEqual({ count: 45, avg_cost: 0.5, }); }); it("should handle missing values", () => { const data: DatabaseRow[] = [ { count: 10 }, { count: 15, avg_cost: 0.6 }, { avg_cost: 0.3 }, ]; const result = calculateSubtotals(data, ["count", "avg_cost"]); expect(result).toEqual({ count: 25, avg_cost: 0.3, }); }); it("should handle empty data", () => { const result = calculateSubtotals([], ["count", "avg_cost"]); expect(result).toEqual({ count: 0, avg_cost: 0, }); }); }); describe("calculateGrandTotals", () => { it("should calculate grand totals for all data", () => { const result = calculateGrandTotals(sampleData, sampleMetrics); expect(result).toEqual({ count: 75, // 10 + 15 + 20 + 25 + 5 avg_cost: 0.5, // (0.5 + 0.6 + 0.2 + 0.4 + 0.8) / 5 }); }); it("should handle empty data", () => { const result = calculateGrandTotals([], sampleMetrics); expect(result).toEqual({ count: 0, avg_cost: 0, }); }); }); describe("createSubtotalRow", () => { it("should create properly formatted subtotal row", () => { const subtotalValues = { count: 25, avg_cost: 1.1 }; const result = createSubtotalRow("gpt-4", subtotalValues, 0); expect(result).toEqual({ id: "subtotal-0-subtotal:gpt-4", type: "subtotal", level: 0, label: "gpt-4 (Subtotal)", values: subtotalValues, isSubtotal: true, dimensionValues: { subtotal: "gpt-4" }, }); }); }); describe("createGrandTotalRow", () => { it("should create properly formatted grand total row", () => { const metrics = ["count", "avg_cost"]; const grandTotalValues = { count: 75, avg_cost: 2.6 }; const result = createGrandTotalRow(metrics, grandTotalValues); expect(result).toEqual({ id: "total-0-total:grand", type: "total", level: 0, label: "Total", values: grandTotalValues, isTotal: true, dimensionValues: { total: "grand" }, }); }); }); describe("transformToPivotTable", () => { describe("zero dimensions (summary table)", () => { it("should create single total row when no dimensions specified", () => { const config: PivotTableConfig = { dimensions: [], metrics: sampleMetrics, }; const result = transformToPivotTable(sampleData, config); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: "total", level: 0, label: "Total", isTotal: true, }); expect(result[0].values).toEqual({ count: 75, avg_cost: 0.5, }); }); it("should handle empty data with zero dimensions", () => { const config: PivotTableConfig = { dimensions: [], metrics: sampleMetrics, }; const result = transformToPivotTable([], config); expect(result).toHaveLength(1); expect(result[0].values).toEqual({ count: 0, avg_cost: 0, }); }); }); describe("single dimension grouping", () => { it("should create grouped table with subtotals and grand total", () => { const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, rowLimit: 20, }; const result = transformToPivotTable(sampleData, config); // Should have: 1 grand total + 5 data rows = 6 rows (no subtotals for single dimension) expect(result).toHaveLength(6); // Check grand total is first expect(result[0]).toMatchObject({ type: "total", level: 0, label: "Total", isTotal: true, }); // Check structure: data rows after total, no subtotals for single dimension expect(result[1]).toMatchObject({ type: "data", level: 0, label: "claude", }); }); it("should apply row limit correctly", () => { const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, rowLimit: 2, // Limit to 2 data rows }; const result = transformToPivotTable(sampleData, config); // Should have: 1 grand total + 2 data rows = 3 rows (no subtotals for single dimension) expect(result).toHaveLength(3); // Verify grand total is first, then only 2 data rows are included expect(result[0].type).toBe("total"); const dataRows = result.filter((row) => row.type === "data"); expect(dataRows).toHaveLength(2); }); }); describe("two-dimension grouping", () => { it("should create nested table with proper indentation and subtotals", () => { const config: PivotTableConfig = { dimensions: ["model", "user"], metrics: sampleMetrics, rowLimit: 20, }; const result = transformToPivotTable(sampleData, config); // Should have grand total, subtotals for first dimension, and data rows expect(result.length).toBeGreaterThan(5); // Check for proper nesting levels const dataRows = result.filter((row) => row.type === "data"); const subtotalRows = result.filter((row) => row.type === "subtotal"); const totalRows = result.filter((row) => row.type === "total"); expect(dataRows.length).toBeGreaterThan(0); expect(subtotalRows.length).toBeGreaterThan(0); expect(totalRows).toHaveLength(1); // Check that grand total is first expect(result[0].type).toBe("total"); // Check indentation levels expect(dataRows.some((row) => row.level === 1)).toBe(true); // Second dimension should be indented at level 1 expect(subtotalRows.every((row) => row.level === 0)).toBe(true); // Subtotals at first level (level 0) }); it("should handle missing values in second dimension", () => { const dataWithMissing: DatabaseRow[] = [ { model: "gpt-4", user: "alice", count: 10, avg_cost: 0.5 }, { model: "gpt-4", count: 15, avg_cost: 0.6 }, // missing user { model: "gpt-3.5", user: "bob", count: 20, avg_cost: 0.3 }, ]; const config: PivotTableConfig = { dimensions: ["model", "user"], metrics: sampleMetrics, rowLimit: 20, }; const result = transformToPivotTable(dataWithMissing, config); // Should handle missing values gracefully expect(result.length).toBeGreaterThan(0); expect(result.some((row) => row.label.includes("n/a"))).toBe(true); }); }); describe("edge cases and error handling", () => { it("should handle empty data gracefully", () => { const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, }; const result = transformToPivotTable([], config); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: "total", level: 0, label: "Total", values: { count: 0, avg_cost: 0 }, }); }); it("should handle data with all null values", () => { const nullData: DatabaseRow[] = [ { model: null, user: null, count: null, avg_cost: null }, { model: null, user: null, count: null, avg_cost: null }, ]; const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, }; const result = transformToPivotTable(nullData, config); expect(result.length).toBeGreaterThan(0); expect(result[0].type).toBe("total"); }); it("should validate configuration before processing", () => { const invalidConfig: PivotTableConfig = { dimensions: Array.from( { length: MAX_PIVOT_TABLE_DIMENSIONS + 1 }, (_, i) => `dim${i + 1}`, ), metrics: [], }; expect(() => transformToPivotTable(sampleData, invalidConfig), ).toThrow(); }); it("should use default row limit when not specified", () => { const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, // No rowLimit specified }; // This should not throw an error and should use DEFAULT_ROW_LIMIT expect(() => transformToPivotTable(sampleData, config)).not.toThrow(); }); it("should handle very large datasets with row limiting", () => { // Create a large dataset const largeData: DatabaseRow[] = Array.from( { length: 100 }, (_, i) => ({ model: `model-${i % 10}`, user: `user-${i % 5}`, count: i + 1, avg_cost: (i + 1) * 0.1, }), ); const config: PivotTableConfig = { dimensions: ["model"], metrics: sampleMetrics, rowLimit: 5, }; const result = transformToPivotTable(largeData, config); // Should limit data rows but still include totals const dataRows = result.filter((row) => row.type === "data"); expect(dataRows.length).toBeLessThanOrEqual(5); // Should still have grand total at the top expect(result[0].type).toBe("total"); }); }); describe("configuration edge cases", () => { it("should handle single metric", () => { const config: PivotTableConfig = { dimensions: ["model"], metrics: ["count"], }; const result = transformToPivotTable(sampleData, config); expect(result.length).toBeGreaterThan(0); expect(Object.keys(result[0].values)).toEqual(["count"]); }); it("should handle many metrics", () => { const manyMetrics = [ "count", "avg_cost", "total_tokens", "avg_latency", ]; const dataWithManyMetrics: DatabaseRow[] = sampleData.map((row) => ({ ...row, total_tokens: 1000, avg_latency: 0.5, })); const config: PivotTableConfig = { dimensions: ["model"], metrics: manyMetrics, }; const result = transformToPivotTable(dataWithManyMetrics, config); expect(result.length).toBeGreaterThan(0); expect(Object.keys(result[0].values)).toEqual(manyMetrics); }); }); }); describe("multiple metrics and aggregation functions", () => { // Test data with various aggregation types const multiMetricData: DatabaseRow[] = [ { environment: "production", model: "gpt-4", count_requests: 100, sum_tokens: 5000, avg_latency: 120, min_cost: 0.05, max_cost: 0.15, p95_duration: 250, }, { environment: "production", model: "gpt-3.5", count_requests: 200, sum_tokens: 8000, avg_latency: 80, min_cost: 0.02, max_cost: 0.08, p95_duration: 150, }, { environment: "staging", model: "gpt-4", count_requests: 50, sum_tokens: 2500, avg_latency: 140, min_cost: 0.06, max_cost: 0.18, p95_duration: 300, }, ]; const multiMetrics = [ "count_requests", "sum_tokens", "avg_latency", "min_cost", "max_cost", "p95_duration", ]; describe("aggregation type detection and calculation", () => { it("should correctly aggregate multiple metrics with different aggregation types", () => { const config: PivotTableConfig = { dimensions: ["environment"], metrics: multiMetrics, }; const result = transformToPivotTable(multiMetricData, config); // Find the grand total row const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { // Count should be summed: 100 + 200 + 50 = 350 expect(totalRow.values.count_requests).toBe(350); // Sum should be summed: 5000 + 8000 + 2500 = 15500 expect(totalRow.values.sum_tokens).toBe(15500); // Average should be averaged: (120 + 80 + 140) / 3 = 113.33... expect(totalRow.values.avg_latency).toBeCloseTo(113.33, 2); // Min should be minimum: min(0.05, 0.02, 0.06) = 0.02 expect(totalRow.values.min_cost).toBe(0.02); // Max should be maximum: max(0.15, 0.08, 0.18) = 0.18 expect(totalRow.values.max_cost).toBe(0.18); // Percentile should be averaged: (250 + 150 + 300) / 3 = 233.33... expect(totalRow.values.p95_duration).toBeCloseTo(233.33, 2); } }); it("should correctly calculate subtotals with different aggregation types", () => { const config: PivotTableConfig = { dimensions: ["environment"], metrics: multiMetrics, }; const result = transformToPivotTable(multiMetricData, config); // Since this is single dimension, there should be data rows and a total const dataRows = result.filter((row) => row.type === "data"); const productionDataRows = dataRows.filter( (row) => row.label.includes("production") || (row.dimensionValues && row.dimensionValues.environment === "production"), ); // There should be 2 production data rows expect(productionDataRows.length).toBeGreaterThan(0); // Check total row calculations include production data correctly const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { // Production contributes: count=300, sum=13000, avg=(120+80)/2=100 // Staging contributes: count=50, sum=2500, avg=140 // Total should be: count=350, sum=15500, avg=(100+140)/2=120 (weighted by count would be different) expect(totalRow.values.count_requests).toBe(350); expect(totalRow.values.sum_tokens).toBe(15500); } }); it("should handle mixed numeric and string values correctly", () => { const mixedData: DatabaseRow[] = [ { environment: "prod", count_requests: "100", // String value avg_latency: 120.5, // Numeric value sum_tokens: "5000", // String value }, { environment: "prod", count_requests: 200, // Numeric value avg_latency: "80.2", // String value sum_tokens: 8000, // Numeric value }, ]; const config: PivotTableConfig = { dimensions: ["environment"], metrics: ["count_requests", "avg_latency", "sum_tokens"], }; const result = transformToPivotTable(mixedData, config); const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { // Count: 100 + 200 = 300 (strings parsed correctly) expect(totalRow.values.count_requests).toBe(300); // Average: (120.5 + 80.2) / 2 = 100.35 expect(totalRow.values.avg_latency).toBeCloseTo(100.35, 2); // Sum: 5000 + 8000 = 13000 (strings parsed correctly) expect(totalRow.values.sum_tokens).toBe(13000); } }); it("should handle edge cases for different aggregation types", () => { const edgeCaseData: DatabaseRow[] = [ { category: "A", count_items: 0, avg_score: 0, min_value: 100, max_value: 100, }, ]; const config: PivotTableConfig = { dimensions: ["category"], metrics: ["count_items", "avg_score", "min_value", "max_value"], }; const result = transformToPivotTable(edgeCaseData, config); const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { expect(totalRow.values.count_items).toBe(0); expect(totalRow.values.avg_score).toBe(0); expect(totalRow.values.min_value).toBe(100); expect(totalRow.values.max_value).toBe(100); } }); it("should handle empty arrays for aggregation functions", () => { const config: PivotTableConfig = { dimensions: ["nonexistent"], metrics: ["count_requests", "avg_latency"], }; const result = transformToPivotTable([], config); expect(result).toHaveLength(1); const totalRow = result[0]; expect(totalRow.type).toBe("total"); expect(totalRow.values.count_requests).toBe(0); expect(totalRow.values.avg_latency).toBe(0); }); }); describe("complex multi-dimensional scenarios with multiple metrics", () => { it("should handle two dimensions with multiple aggregation types", () => { const config: PivotTableConfig = { dimensions: ["environment", "model"], metrics: multiMetrics, }; const result = transformToPivotTable(multiMetricData, config); // Should have grand total, subtotals, and data rows const dataRows = result.filter((row) => row.type === "data"); const subtotalRows = result.filter((row) => row.type === "subtotal"); const totalRow = result.find((row) => row.type === "total"); expect(dataRows.length).toBe(3); // 3 data points expect(subtotalRows.length).toBeGreaterThan(0); // Environment subtotals expect(totalRow).toBeDefined(); // Check that grand total is first expect(result[0].type).toBe("total"); // Check that subtotals correctly aggregate their children const productionSubtotal = subtotalRows.find((row) => row.label.includes("production"), ); if (productionSubtotal) { // Production has 2 models: gpt-4 (100 requests) + gpt-3.5 (200 requests) = 300 expect(productionSubtotal.values.count_requests).toBe(300); // Production tokens: 5000 + 8000 = 13000 expect(productionSubtotal.values.sum_tokens).toBe(13000); // Production avg latency: (120 + 80) / 2 = 100 expect(productionSubtotal.values.avg_latency).toBe(100); // Production min cost: min(0.05, 0.02) = 0.02 expect(productionSubtotal.values.min_cost).toBe(0.02); // Production max cost: max(0.15, 0.08) = 0.15 expect(productionSubtotal.values.max_cost).toBe(0.15); } }); it("should validate metric field name patterns", () => { const testCases = [ { metric: "count_requests", expectedType: "count" }, { metric: "sum_tokens", expectedType: "sum" }, { metric: "avg_latency", expectedType: "avg" }, { metric: "average_score", expectedType: "avg" }, { metric: "min_cost", expectedType: "min" }, { metric: "max_duration", expectedType: "max" }, { metric: "p95_latency", expectedType: "percentile" }, { metric: "p99_duration", expectedType: "percentile" }, { metric: "p50_response", expectedType: "percentile" }, { metric: "unknown_metric", expectedType: "sum" }, // defaults to sum ]; // Test each pattern by creating data and checking the results testCases.forEach(({ metric, expectedType }) => { const testData: DatabaseRow[] = [ { category: "A", [metric]: 10 }, { category: "A", [metric]: 20 }, ]; const config: PivotTableConfig = { dimensions: ["category"], metrics: [metric], }; const result = transformToPivotTable(testData, config); const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { const value = totalRow.values[metric]; switch (expectedType) { case "count": case "sum": expect(value).toBe(30); // 10 + 20 break; case "avg": case "percentile": expect(value).toBe(15); // (10 + 20) / 2 break; case "min": expect(value).toBe(10); // min(10, 20) break; case "max": expect(value).toBe(20); // max(10, 20) break; } } }); }); it("should maintain precision in floating point calculations", () => { const precisionData: DatabaseRow[] = [ { env: "test", avg_value: 0.1 }, { env: "test", avg_value: 0.2 }, { env: "test", avg_value: 0.3 }, ]; const config: PivotTableConfig = { dimensions: ["env"], metrics: ["avg_value"], }; const result = transformToPivotTable(precisionData, config); const totalRow = result.find((row) => row.type === "total"); expect(totalRow).toBeDefined(); if (totalRow) { // Should be (0.1 + 0.2 + 0.3) / 3 = 0.2, properly rounded expect(totalRow.values.avg_value).toBeCloseTo(0.2, 10); } }); }); }); describe("getNextSortState", () => { const defaultSort = { column: "avg_cost", order: "DESC" as const }; describe("no current sort", () => { test("returns DESC when no current sort", () => { const next = getNextSortState(null, null, "count"); expect(next).toEqual({ column: "count", order: "DESC" }); }); test("returns DESC when no current sort with default sort defined", () => { const next = getNextSortState(defaultSort, null, "count"); expect(next).toEqual({ column: "count", order: "DESC" }); }); }); describe("different column", () => { test("returns DESC when different column", () => { const next = getNextSortState( null, { column: "metric", order: "ASC" }, "count", ); expect(next).toEqual({ column: "count", order: "DESC" }); }); test("returns DESC when different column with default sort", () => { const next = getNextSortState( defaultSort, { column: "metric", order: "ASC" }, "count", ); expect(next).toEqual({ column: "count", order: "DESC" }); }); }); describe("non-default column cycling", () => { test("non-default column: DESC -> ASC", () => { const next = getNextSortState( defaultSort, { column: "count", order: "DESC" }, "count", ); expect(next).toEqual({ column: "count", order: "ASC" }); }); test("non-default column: ASC -> default sort", () => { const next = getNextSortState( defaultSort, { column: "count", order: "ASC" }, "count", ); expect(next).toEqual(defaultSort); }); test("non-default column: ASC -> null when no default sort", () => { const next = getNextSortState( null, { column: "count", order: "ASC" }, "count", ); expect(next).toBeNull(); }); }); describe("default column cycling", () => { test("default column: DESC -> ASC", () => { const next = getNextSortState( defaultSort, { column: "avg_cost", order: "DESC" }, "avg_cost", ); expect(next).toEqual({ column: "avg_cost", order: "ASC" }); }); test("default column: ASC -> DESC (infinite cycle)", () => { const next = getNextSortState( defaultSort, { column: "avg_cost", order: "ASC" }, "avg_cost", ); expect(next).toEqual({ column: "avg_cost", order: "DESC" }); }); }); describe("edge cases", () => { test("handles undefined default sort", () => { const next = getNextSortState( undefined as any, { column: "count", order: "ASC" }, "count", ); expect(next).toBeNull(); }); test("handles null default sort", () => { const next = getNextSortState( null, { column: "count", order: "ASC" }, "count", ); expect(next).toBeNull(); }); test("handles null current sort with defined default", () => { const next = getNextSortState(defaultSort, null, "count"); expect(next).toEqual({ column: "count", order: "DESC" }); }); }); }); });