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';
|
|
|
|
|
import { LineCounterEntry } from './types';
|
2025-03-26 11:57:57 +08:00
|
|
|
import {
|
|
|
|
|
extractPixelValue,
|
|
|
|
|
frameToRecords,
|
|
|
|
|
getAlignmentFactor,
|
|
|
|
|
getCellColors,
|
|
|
|
|
getCellLinks,
|
|
|
|
|
getCellOptions,
|
|
|
|
|
getComparator,
|
|
|
|
|
getDefaultRowHeight,
|
|
|
|
|
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,
|
|
|
|
|
buildRowLineCounters,
|
|
|
|
|
buildHeaderLineCounters,
|
|
|
|
|
getTextLineEstimator,
|
|
|
|
|
createTypographyContext,
|
2025-07-26 05:43:36 +08:00
|
|
|
applySort,
|
2025-07-29 05:03:55 +08:00
|
|
|
SINGLE_LINE_ESTIMATE_THRESHOLD,
|
2025-08-02 00:27:53 +08:00
|
|
|
wrapUwrapCount,
|
|
|
|
|
getDataLinksCounter,
|
|
|
|
|
getPillLineCounter,
|
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;
|
|
|
|
|
|
|
|
|
|
it('should handle color background mode', () => {
|
2025-08-02 00:27:53 +08:00
|
|
|
const field = { 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
|
|
|
|
|
|
|
|
const colors = getCellColors(theme, field, displayValue);
|
|
|
|
|
expect(colors.bgColor).toBe('rgb(255, 0, 0)');
|
|
|
|
|
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle color background gradient mode', () => {
|
|
|
|
|
const field = {
|
|
|
|
|
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
|
|
|
|
|
|
|
|
const colors = getCellColors(theme, field, displayValue);
|
|
|
|
|
expect(colors.bgColor).toBe('linear-gradient(120deg, rgb(255, 54, 36), #ff0000)');
|
|
|
|
|
expect(colors.textColor).toBe('rgb(247, 248, 250)');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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-02 00:27:53 +08:00
|
|
|
custom: { cellOptions: { type: TableCellDisplayMode.ColorText, inspectEnabled: false, wrapText: true } },
|
2025-03-26 11:57:57 +08:00
|
|
|
},
|
|
|
|
|
values: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const options = getCellOptions(field);
|
|
|
|
|
|
2025-08-02 00:27:53 +08:00
|
|
|
expect(options).toEqual({ type: TableCellDisplayMode.ColorText, inspectEnabled: false, wrapText: true });
|
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);
|
|
|
|
|
expect(extractPixelValue(null as any)).toBe(0);
|
|
|
|
|
expect(extractPixelValue(undefined as any)).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getDefaultRowHeight', () => {
|
|
|
|
|
const theme = createTheme();
|
|
|
|
|
|
|
|
|
|
it('returns correct height for TableCellHeight.Sm', () => {
|
|
|
|
|
const result = getDefaultRowHeight(theme, TableCellHeight.Sm);
|
|
|
|
|
expect(result).toBe(36);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns correct height for TableCellHeight.Md', () => {
|
|
|
|
|
const result = getDefaultRowHeight(theme, TableCellHeight.Md);
|
|
|
|
|
expect(result).toBe(42);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns correct height for TableCellHeight.Lg', () => {
|
|
|
|
|
const result = getDefaultRowHeight(theme, TableCellHeight.Lg);
|
|
|
|
|
expect(result).toBe(TABLE.MAX_CELL_HEIGHT);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calculates height based on theme when cellHeight is undefined', () => {
|
|
|
|
|
const result = getDefaultRowHeight(theme, undefined as unknown as TableCellHeight);
|
|
|
|
|
|
|
|
|
|
// 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-07-29 05:03:55 +08:00
|
|
|
wrappedCount: expect.any(Function),
|
|
|
|
|
estimateLines: expect.any(Function),
|
|
|
|
|
avgCharWidth: expect.any(Number),
|
|
|
|
|
})
|
|
|
|
|
);
|
2025-08-02 00:27:53 +08:00
|
|
|
expect(ctx.wrappedCount('the quick brown fox jumps over the lazy dog', 100, field, 0)).toEqual(
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
expect(ctx.estimateLines('the quick brown fox jumps over the lazy dog', 100, field, 0)).toEqual(
|
|
|
|
|
expect.any(Number)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('wrapUwrapCount', () => {
|
|
|
|
|
const field: Field = { name: 'test', type: FieldType.string, config: {}, values: ['foo', 'bar', 'baz'] };
|
|
|
|
|
|
|
|
|
|
it('wraps the uwrap count function', () => {
|
|
|
|
|
const wrappedCount = wrapUwrapCount(jest.fn(() => 2));
|
|
|
|
|
expect(wrappedCount('test string', 100, field, 0)).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 1 for null or undefined values', () => {
|
|
|
|
|
const wrappedCount = wrapUwrapCount(jest.fn(() => 2));
|
|
|
|
|
expect(wrappedCount(null, 100, field, 0)).toBe(1);
|
|
|
|
|
expect(wrappedCount(undefined, 100, field, 0)).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
|
|
|
describe('getTextLineEstimator', () => {
|
|
|
|
|
const counter = getTextLineEstimator(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-02 00:27:53 +08:00
|
|
|
expect(counter('asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf', 5, field, 0)).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-02 00:27:53 +08:00
|
|
|
expect(counter('asdfas dfasdfasdf asdfasdfasdfa sdfasdfasdfasdf 23', 200, field, 0)).toBe(2.5);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getDataLinksCounter', () => {
|
|
|
|
|
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'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const counter = getDataLinksCounter();
|
|
|
|
|
expect(counter('my value', 100, field, 0)).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getPillLineCounter', () => {
|
|
|
|
|
it('counts up the number of lines using the pill measuring method', () => {
|
|
|
|
|
const counter = getPillLineCounter(jest.fn((str) => str.length * 5));
|
|
|
|
|
expect(counter('tag1,tag2', 100, {} as Field, 0)).toBe(1);
|
|
|
|
|
expect(counter('tag1,tag2,tag3,tag4,tag5,tag6', 100, {} as Field, 0)).toBe(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 0 if value is null', () => {
|
|
|
|
|
const counter = getPillLineCounter(jest.fn((str) => str.length * 5));
|
|
|
|
|
expect(counter(null, 100, {} as Field, 0)).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns 0 if no pills are inferred', () => {
|
|
|
|
|
const counter = getPillLineCounter(jest.fn((str) => str.length * 5));
|
|
|
|
|
expect(counter('', 100, {} as Field, 0)).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('caches the width measurement for the same value', () => {
|
|
|
|
|
const widthMeasurement = jest.fn((str) => str.length * 5);
|
|
|
|
|
const counter = getPillLineCounter(widthMeasurement);
|
|
|
|
|
counter('tag1,tag2,tag3,tag4,tag5,tag6', 100, {} as Field, 0);
|
|
|
|
|
counter('tag1,tag2', 100, {} as Field, 0);
|
|
|
|
|
counter('tag2', 200, {} as Field, 0);
|
|
|
|
|
counter('tag2,tag3,tag2,tag4,tag4,tag2,tag5', 300, {} as Field, 0);
|
|
|
|
|
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-07-29 05:03:55 +08:00
|
|
|
describe('buildHeaderLineCounters', () => {
|
|
|
|
|
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,
|
|
|
|
|
wrappedCount: jest.fn(() => 2),
|
|
|
|
|
estimateLines: jest.fn(() => 2),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('returns an array of line counters for each column', () => {
|
|
|
|
|
const fields: Field[] = [
|
|
|
|
|
{ name: 'Name', type: FieldType.string, values: [], config: { custom: { wrapHeaderText: true } } },
|
|
|
|
|
{ name: 'Age', type: FieldType.number, values: [], config: { custom: { wrapHeaderText: true } } },
|
|
|
|
|
];
|
|
|
|
|
const counters = buildHeaderLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].counter).toEqual(expect.any(Function));
|
|
|
|
|
expect(counters![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 } } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const counters = buildHeaderLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].fieldIdxs).toEqual([1]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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: {} } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const counters = buildHeaderLineCounters(fields, ctx);
|
|
|
|
|
expect(counters).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('buildRowLineCounters', () => {
|
|
|
|
|
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,
|
|
|
|
|
wrappedCount: jest.fn(() => 2),
|
|
|
|
|
estimateLines: jest.fn(() => 2),
|
|
|
|
|
avgCharWidth: 7,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-02 00:27:53 +08:00
|
|
|
it('sets up text line counters for each text column if wrapping is on', () => {
|
2025-07-29 05:03:55 +08:00
|
|
|
const fields: Field[] = [
|
|
|
|
|
{ name: 'Name', type: FieldType.string, values: [], config: { custom: { cellOptions: { 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: [],
|
|
|
|
|
config: { custom: { cellOptions: { wrapText: true } } },
|
2025-07-04 03:21:58 +08:00
|
|
|
},
|
2025-07-29 05:03:55 +08:00
|
|
|
];
|
|
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].counter).toEqual(expect.any(Function));
|
|
|
|
|
expect(counters![0].fieldIdxs).toEqual([0, 1]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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: [],
|
|
|
|
|
config: { custom: { cellOptions: { wrapText: true } } },
|
2025-07-04 03:21:58 +08:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2025-07-29 05:03:55 +08:00
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].fieldIdxs).toEqual([1]);
|
|
|
|
|
});
|
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"]'],
|
|
|
|
|
config: { custom: { cellOptions: { type: TableCellDisplayMode.Pill, wrapText: true } } },
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].estimate).toEqual(expect.any(Function));
|
|
|
|
|
expect(counters![0].estimate!('tag1,tag2', 100, fields[0], 0)).toEqual(expect.any(Number));
|
|
|
|
|
expect(counters![0].counter).toEqual(expect.any(Function));
|
|
|
|
|
expect(counters![0].counter('tag1,tag2', 100, fields[0], 0)).toEqual(expect.any(Number));
|
|
|
|
|
expect(counters![0].fieldIdxs).toEqual([0]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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'],
|
|
|
|
|
config: { custom: { cellOptions: { type: TableCellDisplayMode.DataLinks, wrapText: true } } },
|
|
|
|
|
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' } },
|
|
|
|
|
]),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
expect(counters![0].counter).toEqual(expect.any(Function));
|
|
|
|
|
expect(counters![0].counter('http://example.com/1', 100, fields[0], 0)).toEqual(expect.any(Number));
|
|
|
|
|
expect(counters![0].fieldIdxs).toEqual([0]);
|
|
|
|
|
});
|
|
|
|
|
|
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: {} } },
|
|
|
|
|
{ name: 'Age', type: FieldType.number, values: [], config: { custom: { cellOptions: { wrapText: true } } } },
|
|
|
|
|
];
|
2025-07-04 03:21:58 +08:00
|
|
|
|
2025-07-29 05:03:55 +08:00
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
// empty array - we had one column that indicated it wraps, but it was numeric, so we just ignore it
|
|
|
|
|
expect(counters).toEqual([]);
|
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: {} } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const counters = buildRowLineCounters(fields, ctx);
|
|
|
|
|
expect(counters).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getRowHeight', () => {
|
|
|
|
|
let fields: Field[];
|
|
|
|
|
let counters: LineCounterEntry[];
|
|
|
|
|
|
|
|
|
|
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'],
|
|
|
|
|
config: { custom: { cellOptions: { 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],
|
|
|
|
|
config: { custom: { cellOptions: { wrapText: true } } },
|
2025-07-04 03:21:58 +08:00
|
|
|
},
|
|
|
|
|
];
|
2025-07-29 05:03:55 +08:00
|
|
|
counters = [
|
|
|
|
|
{ counter: jest.fn((value, _length: number) => String(value).split(' ').length), fieldIdxs: [0] }, // Mocked to count words as lines
|
|
|
|
|
{ counter: jest.fn((value, _length: number) => Math.ceil(String(value).length / 3)), fieldIdxs: [1] }, // Mocked to return a line for every 3 digits of a number
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use the default height for single-line rows', () => {
|
|
|
|
|
// 1 line @ 20px, 10px vertical padding = 30, minimum is 36
|
|
|
|
|
expect(getRowHeight(fields, 0, [30, 30], 36, counters, 20, 10)).toBe(36);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
expect(getRowHeight(fields, 3, [30, 30], 36, counters, 5, 5)).toBe(36);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the row height using line counters for multi-line', () => {
|
|
|
|
|
// 3 lines @ 20px ('longer', 'one', 'here'), 10px vertical padding
|
|
|
|
|
expect(getRowHeight(fields, 3, [30, 30], 36, counters, 20, 10)).toBe(70);
|
|
|
|
|
|
|
|
|
|
// 4 lines @ 15px (789 122 349 932), 15px vertical padding
|
|
|
|
|
expect(getRowHeight(fields, 4, [30, 30], 36, counters, 15, 15)).toBe(75);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should take colWidths into account when calculating max wrap cell', () => {
|
|
|
|
|
getRowHeight(fields, 3, [50, 60], 36, counters, 20, 10);
|
2025-08-02 00:27:53 +08:00
|
|
|
expect(counters[0].counter).toHaveBeenCalledWith('longer one here', 50, fields[0], 3);
|
|
|
|
|
expect(counters[1].counter).toHaveBeenCalledWith(123456, 60, fields[1], 3);
|
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', () => {
|
|
|
|
|
getRowHeight(fields, -1, [50, 60], 36, counters, 20, 10);
|
2025-08-02 00:27:53 +08:00
|
|
|
expect(counters[0].counter).toHaveBeenCalledWith('Name', 50, fields[0], -1);
|
|
|
|
|
expect(counters[1].counter).toHaveBeenCalledWith('Age', 60, fields[1], -1);
|
2025-07-29 05:03:55 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore columns which do not have line counters', () => {
|
|
|
|
|
const height = getRowHeight(fields, 3, [30, 30], 36, [counters[1]], 20, 10);
|
|
|
|
|
// 2 lines @ 20px, 10px vertical padding (not 3 lines, since we don't line count Name)
|
|
|
|
|
expect(height).toBe(50);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the default height if there are no counters to apply', () => {
|
|
|
|
|
const height = getRowHeight(fields, 3, [30, 30], 36, [], 20, 10);
|
|
|
|
|
expect(height).toBe(36);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('estimations vs. precise counts', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
counters = [
|
|
|
|
|
{ counter: jest.fn((value, _length: number) => String(value).split(' ').length), fieldIdxs: [0] }, // Mocked to count words as lines
|
|
|
|
|
{
|
|
|
|
|
estimate: jest.fn((value) => String(value).length), // Mocked to return a line for every digits of a number
|
|
|
|
|
counter: jest.fn((value, _length: number) => Math.ceil(String(value).length / 3)),
|
|
|
|
|
fieldIdxs: [1],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2 lines @ 20px (123,456), 10px vertical padding. when we did this before, 'longer one here' would win, making it 70px.
|
|
|
|
|
// the `estimate` function is picking `123456` as the longer one now (6 lines), then the `counter` function is used
|
|
|
|
|
// 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', () => {
|
|
|
|
|
expect(getRowHeight(fields, 3, [30, 30], 36, counters, 20, 10)).toBe(50);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns doesnt bother getting the precise count if the estimates are all below the threshold', () => {
|
|
|
|
|
jest.mocked(counters[0].counter).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - 0.3);
|
|
|
|
|
jest.mocked(counters[1].estimate!).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - 0.1);
|
2025-07-04 03:21:58 +08:00
|
|
|
|
2025-07-29 05:03:55 +08:00
|
|
|
expect(getRowHeight(fields, 3, [30, 30], 36, counters, 20, 10)).toBe(36);
|
2025-07-04 03:21:58 +08:00
|
|
|
|
2025-07-29 05:03:55 +08:00
|
|
|
// this is what we really care about - we want to save on performance by not calling the counter in this case.
|
|
|
|
|
expect(counters[1].counter).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses the precise count if the estimate is above the threshold, even if its below 1', () => {
|
|
|
|
|
// NOTE: if this fails, just change the test to use a different value besides 0.1
|
|
|
|
|
expect(SINGLE_LINE_ESTIMATE_THRESHOLD + 0.1).toBeLessThan(1);
|
2025-07-04 03:21:58 +08:00
|
|
|
|
2025-07-29 05:03:55 +08:00
|
|
|
jest.mocked(counters[0].counter).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD - 0.3);
|
|
|
|
|
jest.mocked(counters[1].estimate!).mockReturnValue(SINGLE_LINE_ESTIMATE_THRESHOLD + 0.1);
|
|
|
|
|
|
|
|
|
|
expect(getRowHeight(fields, 3, [30, 30], 36, counters, 20, 10)).toBe(50);
|
|
|
|
|
});
|
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', () => {
|
|
|
|
|
it.todo('should parse and then stringify string values');
|
|
|
|
|
it.todo('should not throw for non-serializable string values');
|
|
|
|
|
it.todo('should stringify non-string values');
|
|
|
|
|
it.todo('should not throw for non-serializable non-string values');
|
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-03-26 11:57:57 +08:00
|
|
|
});
|