test(plugin-chart-echarts): regression guards for temporal x-axis labels on timeseries charts (#39208)

This commit is contained in:
jesperct
2026-05-05 12:37:35 -03:00
committed by GitHub
parent 8173cfe9e3
commit 5b5f23d127
4 changed files with 313 additions and 0 deletions

View File

@@ -19,11 +19,13 @@
import {
NumberFormats,
SMART_DATE_ID,
SMART_DATE_VERBOSE_ID,
TimeFormatter,
TimeGranularity,
} from '@superset-ui/core';
import {
getPercentFormatter,
getTooltipTimeFormatter,
getXAxisFormatter,
} from '../../src/utils/formatters';
@@ -179,3 +181,53 @@ test('getXAxisFormatter without time grain should use standard smart date behavi
expect(standardResult).toBe(timeGrainResult);
});
// Regression tests for echarts-timeseries-epoch-x-axis-labels investigation.
// The bug report was that temporal x-axis labels could render as "NaN"
// in some edge cases that we could not reproduce locally. The tests below
// lock in the current behavior of the formatters so that a future refactor
// surfaces any change in contract.
test('getTooltipTimeFormatter returns a TimeFormatter with SMART_DATE_VERBOSE id for SMART_DATE_ID', () => {
const formatter = getTooltipTimeFormatter(SMART_DATE_ID);
expect(formatter).toBeInstanceOf(TimeFormatter);
expect((formatter as TimeFormatter).id).toBe(SMART_DATE_VERBOSE_ID);
});
test('getTooltipTimeFormatter returns a TimeFormatter for a custom format string', () => {
const customFormat = '%Y-%m-%d %H:%M';
const formatter = getTooltipTimeFormatter(customFormat);
expect(formatter).toBeInstanceOf(TimeFormatter);
expect((formatter as TimeFormatter).id).toBe(customFormat);
});
test('getTooltipTimeFormatter falls back to the String constructor when no format is supplied', () => {
expect(getTooltipTimeFormatter()).toBe(String);
expect(getTooltipTimeFormatter(undefined)).toBe(String);
});
test('getXAxisFormatter produces stable SMART_DATE output for a valid Date', () => {
// Documents the current happy-path output format so unexpected changes are
// caught during review.
const formatter = getXAxisFormatter(SMART_DATE_ID) as TimeFormatter;
const result = formatter.format(new Date('2025-01-15T00:00:00.000Z'));
expect(typeof result).toBe('string');
expect(result).not.toMatch(/NaN/);
expect(result.length).toBeGreaterThan(0);
});
test('getXAxisFormatter returns a string for an Invalid Date without throwing', () => {
// If a caller ever passes an Invalid Date (the originally-suspected cause
// of epoch-ms axis labels showing NaN in echarts), the formatter must
// still return a string instead of throwing, so echarts does not blow up
// the chart render. The *content* of that string is format-dependent and
// intentionally not asserted here — only that it is a string.
const formatter = getXAxisFormatter(SMART_DATE_ID) as TimeFormatter;
const invalid = new Date(Number.NaN);
expect(() => formatter.format(invalid)).not.toThrow();
expect(typeof formatter.format(invalid)).toBe('string');
const customFormatter = getXAxisFormatter('%Y-%m-%d') as TimeFormatter;
expect(() => customFormatter.format(invalid)).not.toThrow();
expect(typeof customFormatter.format(invalid)).toBe('string');
});

View File

@@ -1419,6 +1419,22 @@ test('getAxisType treats numeric as category for bar charts', () => {
).toEqual(AxisType.Value);
});
test('getAxisType does not coerce Numeric x-axis to Time regardless of values', () => {
// Regression guard for echarts-timeseries-epoch-x-axis-labels investigation:
// getAxisType only considers the coltype reported by the query, never the
// actual values. Numeric coltype must stay on a Value axis so a future
// change that introduces implicit temporal coercion is surfaced here.
expect(getAxisType(false, false, GenericDataType.Numeric)).toEqual(
AxisType.Value,
);
expect(getAxisType(false, false, GenericDataType.Temporal)).toEqual(
AxisType.Time,
);
expect(getAxisType(false, false, GenericDataType.String)).toEqual(
AxisType.Category,
);
});
test('getMinAndMaxFromBounds returns empty object when not truncating', () => {
expect(
getMinAndMaxFromBounds(