| 
									
										
										
										
											2025-07-26 05:43:36 +08:00
										 |  |  | import { SortColumn } from 'react-data-grid'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   createDataFrame, | 
					
						
							|  |  |  |   createTheme, | 
					
						
							|  |  |  |   DataFrame, | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |   DataFrameWithValue, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |   DataLink, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   DisplayValue, | 
					
						
							|  |  |  |   Field, | 
					
						
							|  |  |  |   FieldType, | 
					
						
							|  |  |  |   GrafanaTheme2, | 
					
						
							|  |  |  |   LinkModel, | 
					
						
							|  |  |  |   ValueLinkConfig, | 
					
						
							|  |  |  | } from '@grafana/data'; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | import { BarGaugeDisplayMode, TableCellBackgroundDisplayMode, TableCellHeight } from '@grafana/schema'; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | import { TableCellDisplayMode } from '../types'; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | import { COLUMN, TABLE } from './constants'; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  | import { MeasureCellHeightEntry } from './types'; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | import { | 
					
						
							|  |  |  |   extractPixelValue, | 
					
						
							|  |  |  |   frameToRecords, | 
					
						
							|  |  |  |   getAlignmentFactor, | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  |   getCellColorInlineStylesFactory, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   getCellLinks, | 
					
						
							|  |  |  |   getCellOptions, | 
					
						
							|  |  |  |   getComparator, | 
					
						
							|  |  |  |   getIsNestedTable, | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |   getAlignment, | 
					
						
							|  |  |  |   getJustifyContent, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   migrateTableDisplayModeToCellOptions, | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |   getColumnTypes, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   computeColWidths, | 
					
						
							|  |  |  |   getRowHeight, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   buildCellHeightMeasurers, | 
					
						
							|  |  |  |   buildHeaderHeightMeasurers, | 
					
						
							|  |  |  |   getTextHeightEstimator, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   createTypographyContext, | 
					
						
							| 
									
										
										
										
											2025-07-26 05:43:36 +08:00
										 |  |  |   applySort, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   SINGLE_LINE_ESTIMATE_THRESHOLD, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   getTextHeightMeasurerFromUwrapCount, | 
					
						
							|  |  |  |   getDataLinksHeightMeasurer, | 
					
						
							|  |  |  |   getPillCellHeightMeasurer, | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |   getDefaultRowHeight, | 
					
						
							| 
									
										
										
										
											2025-08-18 22:11:43 +08:00
										 |  |  |   getDisplayName, | 
					
						
							|  |  |  |   predicateByName, | 
					
						
							| 
									
										
										
										
											2025-09-15 23:34:12 +08:00
										 |  |  |   parseStyleJson, | 
					
						
							| 
									
										
										
										
											2025-09-13 05:22:30 +08:00
										 |  |  |   calculateFooterHeight, | 
					
						
							| 
									
										
										
										
											2025-10-02 22:57:07 +08:00
										 |  |  |   displayJsonValue, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | } from './utils'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | describe('TableNG utils', () => { | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |   describe('alignment', () => { | 
					
						
							|  |  |  |     it.each(['left', 'center', 'right'] as const)('should return "%s" when configured', (align) => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(getAlignment({ name: 'Value', type: FieldType.string, values: [], config: { custom: { align } } })).toBe( | 
					
						
							|  |  |  |         align | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |     it.each([ | 
					
						
							|  |  |  |       { type: FieldType.string, align: 'left' }, | 
					
						
							|  |  |  |       { type: FieldType.number, align: 'right' }, | 
					
						
							|  |  |  |       { type: FieldType.boolean, align: 'left' }, | 
					
						
							|  |  |  |       { type: FieldType.time, align: 'left' }, | 
					
						
							|  |  |  |     ])('should return "$align" for field type $type by default', ({ type, align }) => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(getAlignment({ name: 'Test', type, values: [], config: { custom: {} } })).toBe(align); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |     it.each([ | 
					
						
							|  |  |  |       { cellType: undefined, align: 'right' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.Auto, align: 'right' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.ColorText, align: 'right' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.ColorBackground, align: 'right' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.Gauge, align: 'left' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.JSONView, align: 'left' }, | 
					
						
							|  |  |  |       { cellType: TableCellDisplayMode.DataLinks, align: 'left' }, | 
					
						
							|  |  |  |     ])('numeric field should return "$align" for cell type "$cellType"', ({ align, cellType }) => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         getAlignment({ | 
					
						
							|  |  |  |           name: 'Test', | 
					
						
							|  |  |  |           type: FieldType.number, | 
					
						
							|  |  |  |           values: [], | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |           config: { custom: { ...(cellType !== undefined ? { cellOptions: { type: cellType } } : {}) } }, | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |         }) | 
					
						
							|  |  |  |       ).toBe(align); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  |     describe('mapping to getJustifyContent', () => { | 
					
						
							|  |  |  |       it.each([ | 
					
						
							|  |  |  |         { align: 'left', expected: 'flex-start' }, | 
					
						
							|  |  |  |         { align: 'center', expected: 'center' }, | 
					
						
							|  |  |  |         { align: 'right', expected: 'flex-end' }, | 
					
						
							|  |  |  |       ] as const)(`should map align "$align" to justifyContent "$expected"`, ({ align, expected }) => { | 
					
						
							|  |  |  |         expect(getJustifyContent(align)).toBe(expected); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('cell display mode', () => { | 
					
						
							|  |  |  |     const theme = { | 
					
						
							|  |  |  |       colors: { | 
					
						
							|  |  |  |         isDark: true, | 
					
						
							|  |  |  |         mode: 'dark', | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         primary: { text: '#FFFFFF', main: '#FF0000' }, | 
					
						
							|  |  |  |         background: { canvas: '#000000', primary: '#111111' }, | 
					
						
							|  |  |  |         text: { primary: '#FFFFFF' }, | 
					
						
							|  |  |  |         action: { hover: '#FF0000' }, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }, | 
					
						
							|  |  |  |     } as unknown as GrafanaTheme2; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  |     it('should handle color text cell type', () => { | 
					
						
							|  |  |  |       const cellOptions = { | 
					
						
							|  |  |  |         type: TableCellDisplayMode.ColorText as const, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const displayValue = { text: '100', numeric: 100, color: '#ff0000' }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |       const colors = getCellColorInlineStyles(cellOptions, displayValue, false); | 
					
						
							|  |  |  |       expect(colors.color).toBe('#ff0000'); | 
					
						
							|  |  |  |       expect(colors).not.toHaveProperty('background'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should pass thru color background cell type in basic mode', () => { | 
					
						
							|  |  |  |       const cellOptions = { | 
					
						
							|  |  |  |         type: TableCellDisplayMode.ColorBackground as const, | 
					
						
							|  |  |  |         mode: TableCellBackgroundDisplayMode.Basic, | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue = { text: '100', numeric: 100, color: '#ff0000' }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  |       const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |       const colors = getCellColorInlineStyles(cellOptions, displayValue, false); | 
					
						
							|  |  |  |       expect(colors.background).toBe('#ff0000'); | 
					
						
							| 
									
										
										
										
											2025-08-18 22:11:43 +08:00
										 |  |  |       expect(colors.color).toBe('rgb(247, 248, 250)'); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  |     it('should handle color background cell type in gradient mode', () => { | 
					
						
							|  |  |  |       const cellOptions = { | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |         type: TableCellDisplayMode.ColorBackground as const, | 
					
						
							|  |  |  |         mode: TableCellBackgroundDisplayMode.Gradient, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue = { text: '100', numeric: 100, color: '#ff0000' }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  |       const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |       const colors = getCellColorInlineStyles(cellOptions, displayValue, false); | 
					
						
							| 
									
										
										
										
											2025-08-18 22:11:43 +08:00
										 |  |  |       expect(colors.background).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)'); | 
					
						
							|  |  |  |       expect(colors.color).toBe('rgb(247, 248, 250)'); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-08-26 03:47:11 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('does not set CSSProperties for un-mapped cell types', () => { | 
					
						
							|  |  |  |       const cellOptions = { type: TableCellDisplayMode.JSONView as const }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const displayValue = { text: '100', numeric: 100, color: '#ff0000' }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |       const colors = getCellColorInlineStyles(cellOptions, displayValue, false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(colors).toEqual({}); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     describe('applyToRow', () => { | 
					
						
							|  |  |  |       it.each([ | 
					
						
							|  |  |  |         ['hex', '#ffffff00'], | 
					
						
							|  |  |  |         ['rgba', 'rgba(255,255,255,0)'], | 
					
						
							|  |  |  |         ['hsla', 'hsla(0,100%,100%,0)'], | 
					
						
							|  |  |  |       ])( | 
					
						
							|  |  |  |         'should not apply background color if the display value is transparent (%s) and applyToRow is on', | 
					
						
							|  |  |  |         (_format, colorDisplayValue) => { | 
					
						
							|  |  |  |           const cellOptions = { | 
					
						
							|  |  |  |             type: TableCellDisplayMode.ColorBackground as const, | 
					
						
							|  |  |  |             mode: TableCellBackgroundDisplayMode.Basic, | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const displayValue = { text: '100', numeric: 100, color: colorDisplayValue }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |           const colors = getCellColorInlineStyles(cellOptions, displayValue, true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           expect(colors).toEqual({}); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it.each([ | 
					
						
							|  |  |  |         ['hex', '#ffffff00'], | 
					
						
							|  |  |  |         ['rgba', 'rgba(255,255,255,0)'], | 
					
						
							|  |  |  |         ['hsla', 'hsla(0,100%,100%,0)'], | 
					
						
							|  |  |  |       ])( | 
					
						
							|  |  |  |         'should apply background color if the display value is transparent (%s) and applyToRow is off', | 
					
						
							|  |  |  |         (_format, colorDisplayValue) => { | 
					
						
							|  |  |  |           const cellOptions = { | 
					
						
							|  |  |  |             type: TableCellDisplayMode.ColorBackground as const, | 
					
						
							|  |  |  |             mode: TableCellBackgroundDisplayMode.Basic, | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const displayValue = { text: '100', numeric: 100, color: colorDisplayValue }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const getCellColorInlineStyles = getCellColorInlineStylesFactory(theme); | 
					
						
							|  |  |  |           const colors = getCellColorInlineStyles(cellOptions, displayValue, false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           expect(colors).toEqual({ | 
					
						
							|  |  |  |             background: colorDisplayValue, | 
					
						
							|  |  |  |             color: 'rgb(32, 34, 38)', | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('frame to records conversion', () => { | 
					
						
							|  |  |  |     it('should convert DataFrame to TableRows', () => { | 
					
						
							|  |  |  |       const frame = createDataFrame({ | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { name: 'time', values: [1, 2] }, | 
					
						
							|  |  |  |           { name: 'value', values: [10, 20] }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const records = frameToRecords(frame); | 
					
						
							|  |  |  |       expect(records).toHaveLength(2); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(records[0]).toEqual({ __depth: 0, __index: 0, time: 1, value: 10 }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getAlignmentFactor', () => { | 
					
						
							|  |  |  |     it('should create a new alignment factor when none exists', () => { | 
					
						
							|  |  |  |       // Create a field with no existing alignment factor
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.number, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: [1, 22, 333, 4444], | 
					
						
							|  |  |  |         // No state property initially
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         display: (value: unknown) => ({ text: String(value), numeric: Number(value) }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Create a display value
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue: DisplayValue = { text: '1', numeric: 1 }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Call getAlignmentFactor with the first row
 | 
					
						
							|  |  |  |       const result = getAlignmentFactor(field, displayValue, 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify the result has the text property
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual(expect.objectContaining({ text: '1' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Verify that field.state was created and contains the alignment factor
 | 
					
						
							|  |  |  |       expect(field.state).toBeDefined(); | 
					
						
							|  |  |  |       expect(field.state?.alignmentFactors).toBeDefined(); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(field.state?.alignmentFactors).toEqual(expect.objectContaining({ text: '1' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should update alignment factor when a longer value is found', () => { | 
					
						
							|  |  |  |       // Create a field with an existing alignment factor
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.number, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: [1, 22, 333, 4444], | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         state: { alignmentFactors: { text: '1' } }, | 
					
						
							|  |  |  |         display: (value: unknown) => ({ text: String(value), numeric: Number(value) }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Create a display value that is longer than the existing alignment factor
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue: DisplayValue = { text: '4444', numeric: 4444 }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Call getAlignmentFactor
 | 
					
						
							|  |  |  |       const result = getAlignmentFactor(field, displayValue, 3); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify the result is updated to the longer value
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual(expect.objectContaining({ text: '4444' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Verify that field.state.alignmentFactors was updated
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(field.state?.alignmentFactors).toEqual(expect.objectContaining({ text: '4444' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should not update alignment factor when a shorter value is found', () => { | 
					
						
							|  |  |  |       // Create a field with an existing alignment factor for a long value
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.number, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: [1, 22, 333, 4444], | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         state: { alignmentFactors: { text: '4444' } }, | 
					
						
							|  |  |  |         display: (value: unknown) => ({ text: String(value), numeric: Number(value) }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Create a display value that is shorter than the existing alignment factor
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue: DisplayValue = { text: '1', numeric: 1 }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Call getAlignmentFactor
 | 
					
						
							|  |  |  |       const result = getAlignmentFactor(field, displayValue, 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify the result is still the longer value
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual(expect.objectContaining({ text: '4444' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Verify that field.state.alignmentFactors was not changed
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(field.state?.alignmentFactors).toEqual(expect.objectContaining({ text: '4444' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should add alignment factor to existing field state', () => { | 
					
						
							|  |  |  |       // Create a field with existing state but no alignment factors yet
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.number, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: [1, 22, 333, 4444], | 
					
						
							|  |  |  |         // Field has state but no alignmentFactors
 | 
					
						
							|  |  |  |         state: { | 
					
						
							|  |  |  |           // Use a valid property for FieldState
 | 
					
						
							|  |  |  |           // For example, if calcs is a valid property:
 | 
					
						
							|  |  |  |           calcs: { sum: 4460 }, | 
					
						
							|  |  |  |           // Or if noValue is a valid property:
 | 
					
						
							|  |  |  |           // noValue: true
 | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         display: (value: unknown) => ({ text: String(value), numeric: Number(value) }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Create a display value
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const displayValue: DisplayValue = { text: '1', numeric: 1 }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Call getAlignmentFactor with the first row
 | 
					
						
							|  |  |  |       const result = getAlignmentFactor(field, displayValue, 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify the result has the text property
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual(expect.objectContaining({ text: '1' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Verify that field.state was preserved and alignment factor was added
 | 
					
						
							|  |  |  |       expect(field.state).toBeDefined(); | 
					
						
							|  |  |  |       // Check for the valid property we used
 | 
					
						
							|  |  |  |       expect(field.state?.calcs).toBeDefined(); | 
					
						
							|  |  |  |       expect(field.state?.alignmentFactors).toBeDefined(); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(field.state?.alignmentFactors).toEqual(expect.objectContaining({ text: '1' })); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it.todo('alignmentFactor.text = displayValue.text;'); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getColumnTypes', () => { | 
					
						
							|  |  |  |     it('builds the expected record with column types', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'name', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							|  |  |  |           display: (v) => ({ text: v as string, numeric: NaN }), | 
					
						
							|  |  |  |           config: {}, | 
					
						
							|  |  |  |           values: [], | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'age', | 
					
						
							|  |  |  |           type: FieldType.number, | 
					
						
							|  |  |  |           display: (v) => ({ text: (v as number).toString(), numeric: v as number }), | 
					
						
							|  |  |  |           config: {}, | 
					
						
							|  |  |  |           values: [], | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'active', | 
					
						
							|  |  |  |           type: FieldType.boolean, | 
					
						
							|  |  |  |           display: (v) => ({ text: (v as boolean).toString(), numeric: NaN }), | 
					
						
							|  |  |  |           config: {}, | 
					
						
							|  |  |  |           values: [], | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  |       const result = getColumnTypes(fields); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual({ name: FieldType.string, age: FieldType.number, active: FieldType.boolean }); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should recursively build column types when nested fields are present', () => { | 
					
						
							|  |  |  |       const frame: DataFrame = { | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             type: FieldType.nestedFrames, | 
					
						
							|  |  |  |             name: 'nestedCol', | 
					
						
							|  |  |  |             config: {}, | 
					
						
							|  |  |  |             values: [ | 
					
						
							|  |  |  |               [ | 
					
						
							|  |  |  |                 createDataFrame({ | 
					
						
							|  |  |  |                   fields: [ | 
					
						
							|  |  |  |                     { name: 'time', values: [1, 2] }, | 
					
						
							|  |  |  |                     { name: 'value', values: [10, 20] }, | 
					
						
							|  |  |  |                   ], | 
					
						
							|  |  |  |                 }), | 
					
						
							|  |  |  |               ], | 
					
						
							|  |  |  |               [ | 
					
						
							|  |  |  |                 createDataFrame({ | 
					
						
							|  |  |  |                   fields: [ | 
					
						
							|  |  |  |                     { name: 'time', values: [3, 4] }, | 
					
						
							|  |  |  |                     { name: 'value', values: [30, 40] }, | 
					
						
							|  |  |  |                   ], | 
					
						
							|  |  |  |                 }), | 
					
						
							|  |  |  |               ], | 
					
						
							|  |  |  |             ], | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         length: 0, | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(getColumnTypes(frame.fields)).toEqual({ | 
					
						
							|  |  |  |         stringCol: FieldType.string, | 
					
						
							|  |  |  |         time: FieldType.time, | 
					
						
							|  |  |  |         value: FieldType.number, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('does not throw if nestedFrames has no values', () => { | 
					
						
							|  |  |  |       const frame: DataFrame = { | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |           { type: FieldType.nestedFrames, name: 'nestedCol', config: {}, values: [] }, | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         ], | 
					
						
							|  |  |  |         length: 0, | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(getColumnTypes(frame.fields)).toEqual({ stringCol: FieldType.string }); | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getIsNestedTable', () => { | 
					
						
							|  |  |  |     it('should detect nested frames', () => { | 
					
						
							|  |  |  |       const frame: DataFrame = { | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, | 
					
						
							|  |  |  |           { type: FieldType.nestedFrames, name: 'nestedCol', config: {}, values: [] }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         length: 0, | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       expect(getIsNestedTable(frame.fields)).toBe(true); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return false for regular frames', () => { | 
					
						
							|  |  |  |       const frame: DataFrame = { | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { type: FieldType.string, name: 'stringCol', config: {}, values: [] }, | 
					
						
							|  |  |  |           { type: FieldType.number, name: 'numberCol', config: {}, values: [] }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         length: 0, | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       expect(getIsNestedTable(frame.fields)).toBe(false); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getComparator', () => { | 
					
						
							|  |  |  |     it('should compare numbers correctly', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.number); | 
					
						
							|  |  |  |       expect(comparator(1, 2)).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator(2, 1)).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator(1, 1)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle undefined values', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.number); | 
					
						
							|  |  |  |       expect(comparator(undefined, 1)).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator(1, undefined)).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator(undefined, undefined)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should compare strings case-insensitively', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.string); | 
					
						
							|  |  |  |       expect(comparator('a', 'B')).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator('B', 'a')).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator('a', 'a')).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle time values', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.time); | 
					
						
							|  |  |  |       const t1 = 1672531200000; // 2023-01-01
 | 
					
						
							|  |  |  |       const t2 = 1672617600000; // 2023-01-02
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(comparator(t1, t2)).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator(t2, t1)).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator(t1, t1)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle boolean values', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.boolean); | 
					
						
							|  |  |  |       expect(comparator(false, true)).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator(true, false)).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator(true, true)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     it('should compare frame values', () => { | 
					
						
							|  |  |  |       const comparator = getComparator(FieldType.frame); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       // simulate using `first`.
 | 
					
						
							|  |  |  |       const frame1: DataFrameWithValue = { | 
					
						
							|  |  |  |         value: 1, | 
					
						
							|  |  |  |         ...createDataFrame({ fields: [{ name: 'a', values: [1, 2, 3, 4] }] }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       const frame2: DataFrameWithValue = { | 
					
						
							|  |  |  |         value: 4, | 
					
						
							|  |  |  |         ...createDataFrame({ fields: [{ name: 'a', values: [4, 3, 2, 1] }] }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       const frame3: DataFrameWithValue = { | 
					
						
							|  |  |  |         value: 4, | 
					
						
							|  |  |  |         ...createDataFrame({ fields: [{ name: 'a', values: [4, 5, 6, 7] }] }), | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       expect(comparator(frame1, frame2)).toBeLessThan(0); | 
					
						
							|  |  |  |       expect(comparator(frame2, frame1)).toBeGreaterThan(0); | 
					
						
							|  |  |  |       expect(comparator(frame2, frame2)).toBe(0); | 
					
						
							|  |  |  |       expect(comparator(frame2, frame3)).toBe(0); // equivalent start values
 | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('migrateTableDisplayModeToCellOptions', () => { | 
					
						
							|  |  |  |     it('should migrate basic to gauge mode', () => { | 
					
						
							|  |  |  |       const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.BasicGauge); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual({ type: TableCellDisplayMode.Gauge, mode: BarGaugeDisplayMode.Basic }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should migrate gradient-gauge to gauge mode with gradient', () => { | 
					
						
							|  |  |  |       const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.GradientGauge); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual({ type: TableCellDisplayMode.Gauge, mode: BarGaugeDisplayMode.Gradient }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should migrate color-background to color background with gradient', () => { | 
					
						
							|  |  |  |       const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.ColorBackground); | 
					
						
							|  |  |  |       expect(result).toEqual({ | 
					
						
							|  |  |  |         type: TableCellDisplayMode.ColorBackground, | 
					
						
							|  |  |  |         mode: TableCellBackgroundDisplayMode.Gradient, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle other display modes', () => { | 
					
						
							|  |  |  |       const result = migrateTableDisplayModeToCellOptions(TableCellDisplayMode.ColorText); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(result).toEqual({ type: TableCellDisplayMode.ColorText }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getCellOptions', () => { | 
					
						
							|  |  |  |     it('should return default options when no custom config is provided', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const field: Field = { name: 'test', type: FieldType.string, config: {}, values: [] }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Check that default options are returned
 | 
					
						
							|  |  |  |       expect(options).toEqual({ type: TableCellDisplayMode.Auto }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should extract cell options from field config', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: { | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           custom: { cellOptions: { type: TableCellDisplayMode.ColorText, inspectEnabled: false } }, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |         }, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |       expect(options).toEqual({ type: TableCellDisplayMode.ColorText, inspectEnabled: false }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle legacy displayMode property', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         config: { custom: { displayMode: 'color-background' } }, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // The legacy displayMode should be converted to the new format
 | 
					
						
							|  |  |  |       expect(options.type).toBe(TableCellDisplayMode.ColorBackground); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should prioritize cellOptions over legacy displayMode', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         config: { custom: { displayMode: 'color-background', cellOptions: { type: TableCellDisplayMode.ColorText } } }, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(options.type).toBe(TableCellDisplayMode.ColorBackground); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle image display mode', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: { | 
					
						
							|  |  |  |           custom: { | 
					
						
							|  |  |  |             cellOptions: { | 
					
						
							|  |  |  |               type: TableCellDisplayMode.Image, | 
					
						
							|  |  |  |               // Add image-specific options if they exist
 | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(options.type).toBe(TableCellDisplayMode.Image); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle JSON display mode', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } }, | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const options = getCellOptions(field); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(options.type).toBe(TableCellDisplayMode.JSONView); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getCellLinks', () => { | 
					
						
							|  |  |  |     it('should return undefined when field has no getLinks function', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const field: Field = { name: 'test', type: FieldType.string, config: {}, values: ['value'] }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 0); | 
					
						
							|  |  |  |       expect(links).toEqual(undefined); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return links from field getLinks function', () => { | 
					
						
							|  |  |  |       const mockLinks: LinkModel[] = [ | 
					
						
							|  |  |  |         { title: 'Link 1', href: 'http://example.com/1', target: '_blank', origin: { datasourceUid: 'test' } }, | 
					
						
							|  |  |  |         { title: 'Link 2', href: 'http://example.com/2', target: '_self', origin: { datasourceUid: 'test' } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1', 'value2'], | 
					
						
							|  |  |  |         getLinks: (config: ValueLinkConfig) => { | 
					
						
							|  |  |  |           return config.valueRowIndex === 0 ? mockLinks : []; | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 0); | 
					
						
							|  |  |  |       expect(links).toEqual(mockLinks); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return empty array for out of bounds index', () => { | 
					
						
							|  |  |  |       const mockLinks: LinkModel[] = [ | 
					
						
							|  |  |  |         { title: 'Link 1', href: 'http://example.com/1', target: '_blank', origin: { datasourceUid: 'test' } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |         getLinks: (config: ValueLinkConfig) => { | 
					
						
							|  |  |  |           return config.valueRowIndex === 0 ? mockLinks : []; | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Index out of bounds
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 1); | 
					
						
							|  |  |  |       expect(links).toEqual([]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle getLinks returning undefined', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |         getLinks: (config: ValueLinkConfig) => { | 
					
						
							|  |  |  |           return []; | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 0); | 
					
						
							|  |  |  |       expect(links).toEqual([]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle different link configurations', () => { | 
					
						
							|  |  |  |       // Create links with different valid configurations
 | 
					
						
							|  |  |  |       const mockLinks: LinkModel[] = [ | 
					
						
							|  |  |  |         // Standard link with href
 | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           title: 'External Link', | 
					
						
							|  |  |  |           href: 'http://example.com/full', | 
					
						
							|  |  |  |           target: '_blank', | 
					
						
							|  |  |  |           origin: { datasourceUid: 'test' }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         // Internal link with onClick handler
 | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           title: 'Internal Link', | 
					
						
							|  |  |  |           href: '', // Empty href for internal links
 | 
					
						
							|  |  |  |           onClick: jest.fn(), | 
					
						
							|  |  |  |           target: '_self', | 
					
						
							|  |  |  |           origin: { datasourceUid: 'test' }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |         getLinks: () => mockLinks, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify links are returned unmodified
 | 
					
						
							|  |  |  |       expect(links).toEqual(mockLinks); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Verify we have both types of links
 | 
					
						
							|  |  |  |       expect(links?.find((link) => link.onClick !== undefined)).toBeDefined(); | 
					
						
							|  |  |  |       expect(links?.find((link) => link.href === 'http://example.com/full')).toBeDefined(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |     it('should bind the onClick handlers', () => { | 
					
						
							|  |  |  |       const onClickHandler = jest.fn(); | 
					
						
							|  |  |  |       // Create links with different valid configurations
 | 
					
						
							|  |  |  |       const mockLinks: LinkModel[] = [ | 
					
						
							|  |  |  |         // Internal link with onClick handler
 | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           title: 'Internal Link', | 
					
						
							|  |  |  |           href: '', // Empty href for internal links
 | 
					
						
							|  |  |  |           onClick: onClickHandler, | 
					
						
							|  |  |  |           target: '_self', | 
					
						
							|  |  |  |           origin: { datasourceUid: 'test' }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |         getLinks: () => mockLinks, | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       const links = getCellLinks(field, 0); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |       const link = links?.[0]; | 
					
						
							|  |  |  |       const event = new MouseEvent('click', { bubbles: true }); | 
					
						
							|  |  |  |       jest.spyOn(event, 'preventDefault'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       link?.onClick?.(event); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(event.preventDefault).toHaveBeenCalled(); | 
					
						
							|  |  |  |       expect(onClickHandler).toHaveBeenCalledWith(event, { field, rowIndex: 0 }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it.each([ | 
					
						
							|  |  |  |       { keyName: 'metaKey', eventOverride: { metaKey: true } }, | 
					
						
							|  |  |  |       { keyName: 'ctrlKey', eventOverride: { ctrlKey: true } }, | 
					
						
							|  |  |  |       { keyName: 'shiftKey', eventOverride: { shiftKey: true } }, | 
					
						
							|  |  |  |     ])( | 
					
						
							|  |  |  |       'should allow open a link in a new tab when $keyName clicked instead of using the handler', | 
					
						
							|  |  |  |       ({ eventOverride }) => { | 
					
						
							|  |  |  |         const onClickHandler = jest.fn(); | 
					
						
							|  |  |  |         // Create links with different valid configurations
 | 
					
						
							|  |  |  |         const mockLinks: LinkModel[] = [ | 
					
						
							|  |  |  |           // Internal link with onClick handler
 | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             title: 'Internal Link', | 
					
						
							|  |  |  |             href: '', // Empty href for internal links
 | 
					
						
							|  |  |  |             onClick: onClickHandler, | 
					
						
							|  |  |  |             target: '_self', | 
					
						
							|  |  |  |             origin: { datasourceUid: 'test' }, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         ]; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         const field: Field = { | 
					
						
							|  |  |  |           name: 'test', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							|  |  |  |           config: {}, | 
					
						
							|  |  |  |           values: ['value1'], | 
					
						
							|  |  |  |           getLinks: () => mockLinks, | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         const links = getCellLinks(field, 0); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         const link = links?.[0]; | 
					
						
							|  |  |  |         const event = new MouseEvent('click', { bubbles: true, ...eventOverride }); | 
					
						
							|  |  |  |         jest.spyOn(event, 'preventDefault'); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         link?.onClick?.(event); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:18:23 +08:00
										 |  |  |         expect(event.preventDefault).not.toHaveBeenCalled(); | 
					
						
							|  |  |  |         expect(onClickHandler).not.toHaveBeenCalled(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2025-07-24 03:39:25 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('should filter out links which contain neither href nor onClick', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |         getLinks: (): LinkModel[] => [ | 
					
						
							|  |  |  |           { title: 'Invalid Link', target: '_blank', origin: { datasourceUid: 'test' } } as LinkModel, // No href or onClick
 | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const links = getCellLinks(field, 0); | 
					
						
							|  |  |  |       expect(links).toEqual([]); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('extractPixelValue', () => { | 
					
						
							|  |  |  |     it('should extract numeric value from pixel string', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('100px')).toBe(100); | 
					
						
							|  |  |  |       expect(extractPixelValue('42px')).toBe(42); | 
					
						
							|  |  |  |       expect(extractPixelValue('0px')).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle numeric input', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue(100)).toBe(100); | 
					
						
							|  |  |  |       expect(extractPixelValue(42)).toBe(42); | 
					
						
							|  |  |  |       expect(extractPixelValue(0)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle string numbers without units', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('100')).toBe(100); | 
					
						
							|  |  |  |       expect(extractPixelValue('42')).toBe(42); | 
					
						
							|  |  |  |       expect(extractPixelValue('0')).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle decimal values', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('100.5px')).toBe(100.5); | 
					
						
							|  |  |  |       expect(extractPixelValue('42.75px')).toBe(42.75); | 
					
						
							|  |  |  |       expect(extractPixelValue(100.5)).toBe(100.5); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle negative values', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('-100px')).toBe(-100); | 
					
						
							|  |  |  |       expect(extractPixelValue('-42px')).toBe(-42); | 
					
						
							|  |  |  |       expect(extractPixelValue(-100)).toBe(-100); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle other CSS units by removing them', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('100em')).toBe(100); | 
					
						
							|  |  |  |       expect(extractPixelValue('42rem')).toBe(42); | 
					
						
							|  |  |  |       expect(extractPixelValue('10vh')).toBe(10); | 
					
						
							|  |  |  |       expect(extractPixelValue('20vw')).toBe(20); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should handle whitespace', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue(' 100px ')).toBe(100); | 
					
						
							|  |  |  |       expect(extractPixelValue(' 42 px ')).toBe(42); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return 0 for invalid input when no default is provided', () => { | 
					
						
							|  |  |  |       expect(extractPixelValue('not-a-number')).toBe(0); | 
					
						
							|  |  |  |       expect(extractPixelValue('px')).toBe(0); | 
					
						
							|  |  |  |       expect(extractPixelValue('')).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getDefaultRowHeight', () => { | 
					
						
							|  |  |  |     const theme = createTheme(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |     it.each([ | 
					
						
							|  |  |  |       { input: TableCellHeight.Sm, expected: 36 }, | 
					
						
							|  |  |  |       { input: TableCellHeight.Md, expected: 42 }, | 
					
						
							|  |  |  |       { input: TableCellHeight.Lg, expected: TABLE.MAX_CELL_HEIGHT }, | 
					
						
							|  |  |  |     ])('returns "$expected" for "$input"', ({ input, expected }) => { | 
					
						
							|  |  |  |       const result = getDefaultRowHeight(theme, [], input); | 
					
						
							|  |  |  |       expect(result).toBe(expected); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |     it('returns "auto" if a field is present with the dynamicHeight cellOption  is false', () => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         getDefaultRowHeight( | 
					
						
							|  |  |  |           theme, | 
					
						
							|  |  |  |           [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               name: 'test1', | 
					
						
							|  |  |  |               type: FieldType.string, | 
					
						
							|  |  |  |               config: {}, | 
					
						
							|  |  |  |               values: ['value1'], | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               name: 'test2', | 
					
						
							|  |  |  |               type: FieldType.string, | 
					
						
							|  |  |  |               config: { custom: { cellOptions: { type: TableCellDisplayMode.Markdown, dynamicHeight: true } } }, | 
					
						
							|  |  |  |               values: ['value1'], | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               name: 'test3', | 
					
						
							|  |  |  |               type: FieldType.number, | 
					
						
							|  |  |  |               config: { custom: { cellOptions: { type: TableCellDisplayMode.JSONView } } }, | 
					
						
							|  |  |  |               values: [3], | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |           ], | 
					
						
							|  |  |  |           TableCellHeight.Sm | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ).toBe('auto'); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('calculates height based on theme when cellHeight is undefined', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 07:56:12 +08:00
										 |  |  |       const result = getDefaultRowHeight(theme, []); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Calculate the expected result based on the theme values
 | 
					
						
							|  |  |  |       const expected = TABLE.CELL_PADDING * 2 + theme.typography.fontSize * theme.typography.body.lineHeight; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(result).toBe(expected); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   describe('createTypographyCtx', () => { | 
					
						
							|  |  |  |     // we can't test the effectiveness of this typography context in unit tests, only that it
 | 
					
						
							|  |  |  |     // actually executed the JS correctly. If you called `count` with a sensible value and width,
 | 
					
						
							|  |  |  |     // it wouldn't give you a very reasonable answer in Jest's DOM environment for some reason.
 | 
					
						
							|  |  |  |     it('creates the context using uwrap', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const field: Field = { name: 'test', type: FieldType.string, config: {}, values: ['foo', 'bar', 'baz'] }; | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       const ctx = createTypographyContext(14, 'sans-serif', 0.15); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       expect(ctx).toEqual( | 
					
						
							|  |  |  |         expect.objectContaining({ | 
					
						
							|  |  |  |           ctx: expect.any(CanvasRenderingContext2D), | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |           fontFamily: 'sans-serif', | 
					
						
							|  |  |  |           letterSpacing: 0.15, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |           measureHeight: expect.any(Function), | 
					
						
							|  |  |  |           estimateHeight: expect.any(Function), | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           avgCharWidth: expect.any(Number), | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(ctx.measureHeight('the quick brown fox jumps over the lazy dog', 100, field, 0, 20)).toEqual( | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         expect.any(Number) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(ctx.estimateHeight('the quick brown fox jumps over the lazy dog', 100, field, 0, 20)).toEqual( | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         expect.any(Number) | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('getTextHeightMeasurerFromUwrapCount', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     const field: Field = { name: 'test', type: FieldType.string, config: {}, values: ['foo', 'bar', 'baz'] }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('wraps the uwrap count function', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measureHeight = getTextHeightMeasurerFromUwrapCount(jest.fn(() => 2)); | 
					
						
							|  |  |  |       expect(measureHeight('test string', 100, field, 0, 20)).toBe(40); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it("returns a single line's height for null or undefined values", () => { | 
					
						
							|  |  |  |       const measureHeight = getTextHeightMeasurerFromUwrapCount(jest.fn(() => 2)); | 
					
						
							|  |  |  |       expect(measureHeight(null, 100, field, 0, 20)).toBe(20); | 
					
						
							|  |  |  |       expect(measureHeight(undefined, 100, field, 0, 20)).toBe(20); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('getTextHeightEstimator', () => { | 
					
						
							|  |  |  |     const estimator = getTextHeightEstimator(10); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     const field: Field = { name: 'test', type: FieldType.string, config: {}, values: ['foo', 'bar', 'baz'] }; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     it('returns -1 if there are no strings or dashes within the string', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(estimator('asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf', 5, field, 0, 22)).toBe(-1); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     it('calculates an approximate rendered height for the text based on the width and avgCharWidth', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(estimator('asdfas dfasdfasdf asdfasdfasdfa sdfasdfasdfasdf 23', 200, field, 0, 20)).toBe(60); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('getDataLinksHeightMeasurer', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     it('counts number of valid links using getCellLinks', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: { | 
					
						
							|  |  |  |           links: [ | 
					
						
							|  |  |  |             { title: 'Link 1', url: 'http://example.com/1' }, | 
					
						
							|  |  |  |             { title: 'Invalid Link' } as DataLink, // No href or onClick
 | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               title: 'Link w', | 
					
						
							|  |  |  |               url: 'asdf', | 
					
						
							|  |  |  |               onClick: jest.fn(() => {}), | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |           ], | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         values: ['value1'], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurer = getDataLinksHeightMeasurer(); | 
					
						
							|  |  |  |       expect(measurer('my value', 100, field, 0, 20)).toBe(40); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('getPillCellHeightMeasurer', () => { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     it('counts up the number of lines using the pill measuring method', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurer = getPillCellHeightMeasurer(jest.fn((str) => str.length * 5)); | 
					
						
							|  |  |  |       expect(measurer('tag1,tag2', 100, {} as Field, 0, 20)).toBe(20); | 
					
						
							|  |  |  |       expect(measurer('tag1,tag2,tag3,tag4,tag5,tag6', 100, {} as Field, 0, 20)).toBe(68); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns 0 if value is null', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurer = getPillCellHeightMeasurer(jest.fn((str) => str.length * 5)); | 
					
						
							|  |  |  |       expect(measurer(null, 100, {} as Field, 0, 20)).toBe(0); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns 0 if no pills are inferred', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurer = getPillCellHeightMeasurer(jest.fn((str) => str.length * 5)); | 
					
						
							|  |  |  |       expect(measurer('', 100, {} as Field, 0, 20)).toBe(0); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('caches the width measurement for the same value', () => { | 
					
						
							|  |  |  |       const widthMeasurement = jest.fn((str) => str.length * 5); | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurer = getPillCellHeightMeasurer(widthMeasurement); | 
					
						
							|  |  |  |       measurer('tag1,tag2,tag3,tag4,tag5,tag6', 100, {} as Field, 0, 20); | 
					
						
							|  |  |  |       measurer('tag1,tag2', 100, {} as Field, 0, 20); | 
					
						
							|  |  |  |       measurer('tag2', 200, {} as Field, 0, 20); | 
					
						
							|  |  |  |       measurer('tag2,tag3,tag2,tag4,tag4,tag2,tag5', 300, {} as Field, 0, 20); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       expect(widthMeasurement).toHaveBeenCalledTimes(6); // Should only call for unique values
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('buildHeaderHeightMeasurers', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     const ctx = { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       fontFamily: 'sans-serif', | 
					
						
							|  |  |  |       letterSpacing: 0.15, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       ctx: {} as CanvasRenderingContext2D, | 
					
						
							|  |  |  |       count: jest.fn(() => 2), | 
					
						
							|  |  |  |       avgCharWidth: 7, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       measureHeight: jest.fn(() => 2), | 
					
						
							|  |  |  |       estimateHeight: jest.fn(() => 2), | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it('returns an array of measurers for each column', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: { wrapHeaderText: true } } }, | 
					
						
							|  |  |  |         { name: 'Age', type: FieldType.number, values: [], config: { custom: { wrapHeaderText: true } } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildHeaderHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].measure).toEqual(expect.any(Function)); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([0, 1]); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     it('does not return the index of columns which are not wrapped', () => { | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |       const fields: Field[] = [ | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: {} } }, | 
					
						
							|  |  |  |         { name: 'Age', type: FieldType.number, values: [], config: { custom: { wrapHeaderText: true } } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildHeaderHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([1]); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns undefined if no columns are wrapped', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: {} } }, | 
					
						
							|  |  |  |         { name: 'Age', type: FieldType.number, values: [], config: { custom: {} } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildHeaderHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers).toBeUndefined(); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |   describe('buildCellHeightMeasurers', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     const ctx = { | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       fontFamily: 'sans-serif', | 
					
						
							|  |  |  |       letterSpacing: 0.15, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       ctx: {} as CanvasRenderingContext2D, | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       measureHeight: jest.fn(() => 2), | 
					
						
							|  |  |  |       estimateHeight: jest.fn(() => 2), | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       avgCharWidth: 7, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it('sets up text height measurers for each text column if wrapping is on', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       const fields: Field[] = [ | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: { wrapText: true } } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           name: 'Address', | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |           type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           values: [], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].measure).toEqual(expect.any(Function)); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([0, 1]); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('does not return the index of columns which are not wrapped', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: {} } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           name: 'Address', | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |           type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           values: [], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([1]); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     it('sets up line counting for pills if present and wrapping is on', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'Tags', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							|  |  |  |           values: ['tag1,tag2', 'tag3', '["tag4","tag5","tag6"]'], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true, cellOptions: { type: TableCellDisplayMode.Pill } } }, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].estimate).toEqual(expect.any(Function)); | 
					
						
							|  |  |  |       expect(measurers![0].estimate!('tag1,tag2', 100, fields[0], 0, 22)).toEqual(expect.any(Number)); | 
					
						
							|  |  |  |       expect(measurers![0].measure).toEqual(expect.any(Function)); | 
					
						
							|  |  |  |       expect(measurers![0].measure('tag1,tag2', 100, fields[0], 0, 22)).toEqual(expect.any(Number)); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([0]); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('sets up line counting for datalinks if present and wrapping is on', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'Links', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							|  |  |  |           values: ['http://example.com/1', 'http://example.com/2'], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true, cellOptions: { type: TableCellDisplayMode.DataLinks } } }, | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |           getLinks: jest.fn((): LinkModel[] => [ | 
					
						
							|  |  |  |             { title: 'Link 1', href: 'http://example.com/1', target: '_blank', origin: { datasourceUid: 'test' } }, | 
					
						
							|  |  |  |             { title: 'Link 2', href: 'http://example.com/2', target: '_self', origin: { datasourceUid: 'test' } }, | 
					
						
							|  |  |  |           ]), | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].measure).toEqual(expect.any(Function)); | 
					
						
							|  |  |  |       expect(measurers![0].measure('http://example.com/1', 100, fields[0], 0, 22)).toEqual(expect.any(Number)); | 
					
						
							|  |  |  |       expect(measurers![0].fieldIdxs).toEqual([0]); | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     it('does not enable text counting for non-string fields', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: {} } }, | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |         { name: 'Age', type: FieldType.number, values: [], config: { custom: { wrapText: true } } }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       // empty array - we had one column that indicated it wraps, but it was numeric, so we just ignore it
 | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |       expect(measurers).toBeUndefined(); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     it('returns an undefined if no columns are wrapped', () => { | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |       const fields: Field[] = [ | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |         { name: 'Name', type: FieldType.string, values: [], config: { custom: {} } }, | 
					
						
							|  |  |  |         { name: 'Age', type: FieldType.number, values: [], config: { custom: {} } }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers).toBeUndefined(); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-08-30 03:10:17 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('clamps by maxHeight if set', () => { | 
					
						
							|  |  |  |       const fields: Field[] = [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           name: 'Tags', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							|  |  |  |           values: ['tag1,tag2', 'tag3', '["tag4","tag5","tag6"]'], | 
					
						
							|  |  |  |           config: { custom: { wrapText: true, cellOptions: { type: TableCellDisplayMode.Pill } } }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							|  |  |  |       const measurers = buildCellHeightMeasurers(fields, ctx); | 
					
						
							|  |  |  |       expect(measurers![0].measure!(fields[0].values[2], 20, fields[0], 2, 100)).toBeGreaterThan(50); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       fields[0].config!.custom!.maxHeight = 50; | 
					
						
							|  |  |  |       const measurersWithMax = buildCellHeightMeasurers(fields, ctx, 50); | 
					
						
							|  |  |  |       expect(measurersWithMax![0].measure!(fields[0].values[2], 20, fields[0], 2, 100)).toBe(50); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('getRowHeight', () => { | 
					
						
							|  |  |  |     let fields: Field[]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     let measurers: MeasureCellHeightEntry[]; | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     beforeEach(() => { | 
					
						
							|  |  |  |       fields = [ | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         { | 
					
						
							|  |  |  |           name: 'Name', | 
					
						
							|  |  |  |           type: FieldType.string, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           values: ['foo', 'bar', 'baz', 'longer one here', 'shorter'], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           name: 'Age', | 
					
						
							|  |  |  |           type: FieldType.number, | 
					
						
							|  |  |  |           values: [1, 2, 3, 123456, 789122349932], | 
					
						
							| 
									
										
										
										
											2025-08-28 04:37:02 +08:00
										 |  |  |           config: { custom: { wrapText: true } }, | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |         }, | 
					
						
							|  |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       measurers = [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           measure: jest.fn( | 
					
						
							|  |  |  |             (value, _length, _field, _rowIdx, lineHeight) => String(value).split(' ').length * lineHeight | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |           fieldIdxs: [0], | 
					
						
							|  |  |  |         }, // Mocked to count words as lines
 | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           measure: jest.fn( | 
					
						
							|  |  |  |             (value, _length, _field, _rowIdx, lineHeight) => Math.ceil(String(value).length / 3) * lineHeight | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |           fieldIdxs: [1], | 
					
						
							|  |  |  |         }, // Mocked to return a line for every 3 digits of a number
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       ]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should use the default height for single-line rows', () => { | 
					
						
							|  |  |  |       // 1 line @ 20px, 10px vertical padding = 30, minimum is 36
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(getRowHeight(fields, 0, [30, 30], 36, measurers, 20, 10)).toBe(36); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should use the default height for multi-line rows which are shorter than the default height', () => { | 
					
						
							|  |  |  |       // 3 lines @ 5px, 5px vertical padding = 20, minimum is 36
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(getRowHeight(fields, 3, [30, 30], 36, measurers, 5, 5)).toBe(36); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it('should return the row height using line measurers for multi-line', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       // 3 lines @ 20px ('longer', 'one', 'here'), 10px vertical padding
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(getRowHeight(fields, 3, [30, 30], 36, measurers, 20, 10)).toBe(70); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // 4 lines @ 15px (789 122 349 932), 15px vertical padding
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       expect(getRowHeight(fields, 4, [30, 30], 36, measurers, 15, 15)).toBe(75); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should take colWidths into account when calculating max wrap cell', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       getRowHeight(fields, 3, [50, 60], 36, measurers, 20, 10); | 
					
						
							|  |  |  |       expect(measurers[0].measure).toHaveBeenCalledWith('longer one here', 50, fields[0], 3, 20); | 
					
						
							|  |  |  |       expect(measurers[1].measure).toHaveBeenCalledWith(123456, 60, fields[1], 3, 20); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // this is used to calc wrapped header height
 | 
					
						
							|  |  |  |     it('should use the display name if the rowIdx is -1', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       getRowHeight(fields, -1, [50, 60], 36, measurers, 20, 10); | 
					
						
							|  |  |  |       expect(measurers[0].measure).toHaveBeenCalledWith('Name', 50, fields[0], -1, 20); | 
					
						
							|  |  |  |       expect(measurers[1].measure).toHaveBeenCalledWith('Age', 60, fields[1], -1, 20); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it('should ignore columns which do not have measurers', () => { | 
					
						
							|  |  |  |       const height = getRowHeight(fields, 3, [30, 30], 36, [measurers[1]], 20, 10); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       // 2 lines @ 20px, 10px vertical padding (not 3 lines, since we don't line count Name)
 | 
					
						
							|  |  |  |       expect(height).toBe(50); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |     it('should return the default height if there are no measurers to apply', () => { | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       const height = getRowHeight(fields, 3, [30, 30], 36, [], 20, 10); | 
					
						
							|  |  |  |       expect(height).toBe(36); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     describe('estimations vs. precise counts', () => { | 
					
						
							|  |  |  |       beforeEach(() => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         measurers = [ | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |             measure: jest.fn( | 
					
						
							|  |  |  |               (value, _length, _field, _rowIdx, lineHeight) => String(value).split(' ').length * lineHeight | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |             fieldIdxs: [0], | 
					
						
							|  |  |  |           }, // Mocked to count words as lines
 | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             measure: jest.fn( | 
					
						
							|  |  |  |               (value, _length, _field, _rowIdx, lineHeight) => Math.ceil(String(value).length / 3) * lineHeight | 
					
						
							|  |  |  |             ), | 
					
						
							|  |  |  |             estimate: jest.fn((value, _length, _field, _rowIdx, lineHeight) => String(value).length * lineHeight), // Mocked to return a line for every digits of a number
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |             fieldIdxs: [1], | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |           }, // Mocked to return a line for every 3 digits of a number
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |         ]; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // 2 lines @ 20px (123,456), 10px vertical padding. when we did this before, 'longer one here' would win, making it 70px.
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |       // the `estimate` function is picking `123456` as the longer one now (6 lines), then the `measure` function is used
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       // to calculate the height (2 lines). this is a very forced case, but we just want to prove that it actually works.
 | 
					
						
							|  |  |  |       it('uses the estimate value rather than the precise value to select the row height', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         expect(getRowHeight(fields, 3, [30, 30], 36, measurers, 20, 10)).toBe(50); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it('returns doesnt bother getting the precise count if the estimates are all below the threshold', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         jest.mocked(measurers[0].measure).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - 0.3); | 
					
						
							|  |  |  |         jest.mocked(measurers[1].estimate!).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - 0.1); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         expect(getRowHeight(fields, 3, [30, 30], 36, measurers, 20, 10)).toBe(36); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         // this is what we really care about - we want to save on performance by not calling the measure in this case.
 | 
					
						
							|  |  |  |         expect(measurers[1].measure).not.toHaveBeenCalled(); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it('uses the precise count if the estimate is above the threshold, even if its below 1', () => { | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         // NOTE: if this fails, just change the test to use a different value besides 1
 | 
					
						
							|  |  |  |         const thresholdOffset = 1; | 
					
						
							|  |  |  |         expect(SINGLE_LINE_ESTIMATE_THRESHOLD + thresholdOffset).toBeLessThan(TABLE.LINE_HEIGHT); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         jest.mocked(measurers[0].measure).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - thresholdOffset); | 
					
						
							|  |  |  |         jest.mocked(measurers[1].estimate!).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD + thresholdOffset); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-21 04:31:20 +08:00
										 |  |  |         expect(getRowHeight(fields, 3, [30, 30], 36, measurers, 20, 10)).toBe(50); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |       }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   describe('computeColWidths', () => { | 
					
						
							|  |  |  |     it('returns the configured widths if all columns set them', () => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         computeColWidths( | 
					
						
							|  |  |  |           [ | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |             { name: 'A', type: FieldType.string, values: [], config: { custom: { width: 100 } } }, | 
					
						
							|  |  |  |             { name: 'B', type: FieldType.string, values: [], config: { custom: { width: 200 } } }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           ], | 
					
						
							|  |  |  |           500 | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ).toEqual([100, 200]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('fills the available space if a column has no width set', () => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         computeColWidths( | 
					
						
							|  |  |  |           [ | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |             { name: 'A', type: FieldType.string, values: [], config: {} }, | 
					
						
							|  |  |  |             { name: 'B', type: FieldType.string, values: [], config: { custom: { width: 200 } } }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           ], | 
					
						
							|  |  |  |           500 | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ).toEqual([300, 200]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('applies minimum width when auto width would dip below it', () => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         computeColWidths( | 
					
						
							|  |  |  |           [ | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |             { name: 'A', type: FieldType.string, values: [], config: { custom: { minWidth: 100 } } }, | 
					
						
							|  |  |  |             { name: 'B', type: FieldType.string, values: [], config: { custom: { minWidth: 100 } } }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           ], | 
					
						
							|  |  |  |           100 | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ).toEqual([100, 100]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should use the global column default width when nothing is set', () => { | 
					
						
							|  |  |  |       expect( | 
					
						
							|  |  |  |         computeColWidths( | 
					
						
							|  |  |  |           [ | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |             { name: 'A', type: FieldType.string, values: [], config: {} }, | 
					
						
							|  |  |  |             { name: 'B', type: FieldType.string, values: [], config: {} }, | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |           ], | 
					
						
							|  |  |  |           // we have two columns but have set the table to the width of one default column.
 | 
					
						
							|  |  |  |           COLUMN.DEFAULT_WIDTH | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |       ).toEqual([COLUMN.DEFAULT_WIDTH, COLUMN.DEFAULT_WIDTH]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-29 05:03:55 +08:00
										 |  |  |   describe('displayJsonValue', () => { | 
					
						
							| 
									
										
										
										
											2025-10-02 22:57:07 +08:00
										 |  |  |     let field: Field; | 
					
						
							|  |  |  |     beforeEach(() => { | 
					
						
							|  |  |  |       field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         state: { displayName: 'Test Display Name' }, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |         display: (val: unknown) => ({ text: String(val), numeric: NaN }), | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should parse and then stringify string values', () => { | 
					
						
							|  |  |  |       expect(displayJsonValue(field)('{"valid": "json"}').text).toBe('{\n "valid": "json"\n}'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should not throw for non-serializable string values', () => { | 
					
						
							|  |  |  |       expect(displayJsonValue(field)('{"invalid": "json').text).toBe('{"invalid": "json'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should stringify non-string values', () => { | 
					
						
							|  |  |  |       expect(displayJsonValue(field)(42).text).toBe('42'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should use the underlying field.display method to format values and return numeric values', () => { | 
					
						
							|  |  |  |       field.display = (val: unknown) => ({ text: `**${val}**`, numeric: Number(val), suffix: 'ms' }); | 
					
						
							|  |  |  |       expect(displayJsonValue(field)(42).text).toBe('**42**ms'); | 
					
						
							|  |  |  |       expect(displayJsonValue(field)(42).numeric).toBe(42); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2025-07-04 03:21:58 +08:00
										 |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-07-26 05:43:36 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   describe('applySort', () => { | 
					
						
							|  |  |  |     it('sorts by nanos', () => { | 
					
						
							|  |  |  |       const frame = createDataFrame({ | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { name: 'time', values: [1, 1, 2], nanos: [100, 99, 0] }, | 
					
						
							|  |  |  |           { name: 'value', values: [10, 20, 30] }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |       const sortColumns: SortColumn[] = [{ columnKey: 'time', direction: 'ASC' }]; | 
					
						
							| 
									
										
										
										
											2025-07-26 05:43:36 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const records = applySort(frameToRecords(frame), frame.fields, sortColumns); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(records).toMatchObject([ | 
					
						
							| 
									
										
										
										
											2025-08-02 00:27:53 +08:00
										 |  |  |         { time: 1, value: 20 }, | 
					
						
							|  |  |  |         { time: 1, value: 10 }, | 
					
						
							|  |  |  |         { time: 2, value: 30 }, | 
					
						
							| 
									
										
										
										
											2025-07-26 05:43:36 +08:00
										 |  |  |       ]); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-08-18 22:11:43 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-09-13 05:22:30 +08:00
										 |  |  |   describe('calculateFooterHeight', () => { | 
					
						
							|  |  |  |     it('should return 0 if no footer is present', () => { | 
					
						
							|  |  |  |       const frame = createDataFrame({ | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { name: 'time', values: [1, 1, 2], nanos: [100, 99, 0] }, | 
					
						
							|  |  |  |           { name: 'value', values: [10, 20, 30] }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(calculateFooterHeight(frame.fields)).toBe(0); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return the height in pixels for the max reducers on a given field', () => { | 
					
						
							|  |  |  |       const frame = createDataFrame({ | 
					
						
							|  |  |  |         fields: [ | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             name: 'time', | 
					
						
							|  |  |  |             values: [1, 1, 2], | 
					
						
							|  |  |  |             nanos: [100, 99, 0], | 
					
						
							|  |  |  |             config: { custom: { footer: { reducers: ['min', 'max', 'count'] } } }, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |           { name: 'value', values: [10, 20, 30], config: { custom: { footer: { reducers: ['min'] } } } }, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       expect(calculateFooterHeight(frame.fields)).toBe(78); // 3 reducers * 22px line height + 12px padding
 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-18 22:11:43 +08:00
										 |  |  |   describe('getDisplayName', () => { | 
					
						
							|  |  |  |     it('should return the display name if set', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         state: { displayName: 'Test Display Name' }, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       expect(getDisplayName(field)).toBe('Test Display Name'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return the field name if no display name is set', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         state: {}, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       expect(getDisplayName(field)).toBe('test'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('predicateByName', () => { | 
					
						
							|  |  |  |     it('should return true for matching field names', () => { | 
					
						
							|  |  |  |       const field: Field = { name: 'test', type: FieldType.string, config: {}, values: [] }; | 
					
						
							|  |  |  |       const predicate = predicateByName('test'); | 
					
						
							|  |  |  |       expect(predicate(field)).toBe(true); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return true for a matching display name', () => { | 
					
						
							|  |  |  |       const field: Field = { | 
					
						
							|  |  |  |         name: 'test', | 
					
						
							|  |  |  |         type: FieldType.string, | 
					
						
							|  |  |  |         config: {}, | 
					
						
							|  |  |  |         state: { displayName: 'Test Display Name' }, | 
					
						
							|  |  |  |         values: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |       const predicate = predicateByName('Test Display Name'); | 
					
						
							|  |  |  |       expect(predicate(field)).toBe(true); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should return false for non-matching field names', () => { | 
					
						
							|  |  |  |       const field: Field = { name: 'test', type: FieldType.string, config: {}, values: [] }; | 
					
						
							|  |  |  |       const predicate = predicateByName('other'); | 
					
						
							|  |  |  |       expect(predicate(field)).toBe(false); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-09-15 23:34:12 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   describe('parseStyleJson', () => { | 
					
						
							|  |  |  |     it('parses the contents of the styleField for this row and returns a style object', () => { | 
					
						
							|  |  |  |       expect(parseStyleJson('{"color":"red"}')).toEqual({ color: 'red' }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it.each([ | 
					
						
							|  |  |  |       { type: 'number', value: 12345 }, | 
					
						
							|  |  |  |       { type: 'boolean', value: true }, | 
					
						
							|  |  |  |       { type: 'null', value: null }, | 
					
						
							|  |  |  |       { type: 'undefined', value: undefined }, | 
					
						
							|  |  |  |       { type: 'object', value: { color: 'red' } }, | 
					
						
							|  |  |  |       { type: 'array', value: ['not', 'a', 'string'] }, | 
					
						
							|  |  |  |     ])('returns void if input is a $type', ({ value }) => { | 
					
						
							|  |  |  |       expect(parseStyleJson(value)).toBeUndefined(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it.each([ | 
					
						
							|  |  |  |       { type: 'array', value: '["not","an","object"]' }, | 
					
						
							|  |  |  |       { type: 'string', value: '"just a string"' }, | 
					
						
							|  |  |  |       { type: 'number', value: '12345' }, | 
					
						
							|  |  |  |       { type: 'boolean', value: 'true' }, | 
					
						
							|  |  |  |       { type: 'null', value: 'null' }, | 
					
						
							|  |  |  |     ])('returns void and does not throw if the parsed JSON is a $type', ({ value }) => { | 
					
						
							|  |  |  |       expect(parseStyleJson(value)).toBeUndefined(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns void and does not throw if this is invalid JSON (but it does console.error)', () => { | 
					
						
							|  |  |  |       jest.spyOn(console, 'error').mockImplementation(); | 
					
						
							|  |  |  |       expect(parseStyleJson('{"mal": "formed}')).toBeUndefined(); | 
					
						
							|  |  |  |       expect(console.error).toHaveBeenCalled(); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('only calls console.error once for a given malformed style', () => { | 
					
						
							|  |  |  |       jest.spyOn(console, 'error').mockImplementation(); | 
					
						
							|  |  |  |       for (let i = 0; i < 100; i++) { | 
					
						
							|  |  |  |         parseStyleJson('{"mal": "formed-in-a-new-way}'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       expect(console.error).toHaveBeenCalledTimes(1); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns an object with invalid style properties, because we do not validate the style properties', () => { | 
					
						
							|  |  |  |       expect(parseStyleJson('{"notARealStyle": "someValue"}')).toEqual({ notARealStyle: 'someValue' }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2025-03-26 11:57:57 +08:00
										 |  |  | }); |