Compare commits

...

2 Commits

Author SHA1 Message Date
Abhi Kumar
316c5f5437 test(dashboards): clear the uPlot handle from the container it was created on
React detaches the container ref before effect cleanups run, so unmount and the empty-data
destroy left the handle on the detached node.
2026-10-27 13:46:29 +05:30
Abhi Kumar
5d28d0b6a0 test(dashboards): expose the uPlot instance on the chart container
A chart is a canvas, so a test cannot read its series, scales or bands any other way. Both the
e2e suite and the panel stories read it.
2026-10-27 00:11:17 +05:30
3 changed files with 79 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ import uPlot, { AlignedData, Options } from 'uplot';
import { usePlotContext } from '../../context/PlotContext';
import { UPlotChartProps } from '../types';
import { prepareAlignedData } from './utils';
import { prepareAlignedData, setInstanceHandle } from './utils';
/**
* Check if dimensions have changed
@@ -48,6 +48,9 @@ export default function UPlotChart({
const { setPlotContextInitialState } = usePlotContext();
const containerRef = useRef<HTMLDivElement>(null);
const plotInstanceRef = useRef<uPlot | null>(null);
// The container the live plot was created on. React detaches `containerRef`
// before effect cleanups run, so destroy clears this one, not the ref.
const plotHostRef = useRef<HTMLDivElement | null>(null);
const prevPropsRef = useRef<UPlotChartProps | null>(null);
/**
@@ -58,6 +61,8 @@ export default function UPlotChart({
onDestroy?.(plotInstanceRef.current);
plotInstanceRef.current.destroy();
plotInstanceRef.current = null;
setInstanceHandle(plotHostRef.current, null);
plotHostRef.current = null;
setPlotContextInitialState({ uPlotInstance: null });
plotRef?.(null);
}
@@ -101,6 +106,8 @@ export default function UPlotChart({
});
plotInstanceRef.current = plot;
plotHostRef.current = containerRef.current;
setInstanceHandle(plotHostRef.current, plot);
}, [
config,
data,

View File

@@ -1,6 +1,6 @@
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { applySpanGapsToAlignedData } from 'lib/uPlotV2/utils/dataUtils';
import { AlignedData } from 'uplot';
import uPlot, { AlignedData } from 'uplot';
export function prepareAlignedData({
data,
@@ -14,3 +14,22 @@ export function prepareAlignedData({
? applySpanGapsToAlignedData(data as AlignedData, seriesSpanGaps)
: (data as AlignedData);
}
/** A chart container carrying its live uPlot instance, for tests and debugging. */
export interface UPlotHostElement extends HTMLDivElement {
__uplot?: uPlot;
}
export function setInstanceHandle(
node: HTMLDivElement | null,
plot: uPlot | null,
): void {
if (!node) {
return;
}
if (plot) {
(node as UPlotHostElement).__uplot = plot;
} else {
delete (node as UPlotHostElement).__uplot;
}
}

View File

@@ -352,6 +352,57 @@ describe('UPlotChart', () => {
});
});
describe('instance handle', () => {
const hostOf = (call: number): HTMLElement & { __uplot?: unknown } =>
mockUPlotConstructor.mock.calls[call][2];
it('hangs the live instance on the container it was created on', () => {
render(
<UPlotChart
config={createMockConfig()}
data={validData}
width={600}
height={400}
/>,
{ wrapper: Wrapper },
);
expect(hostOf(0).__uplot).toBe(instances[0]);
});
it('clears the handle from that container on unmount', () => {
const { unmount } = render(
<UPlotChart
config={createMockConfig()}
data={validData}
width={600}
height={400}
/>,
{ wrapper: Wrapper },
);
const host = hostOf(0);
unmount();
expect(host.__uplot).toBeUndefined();
});
it('clears the handle from that container when the data becomes empty', () => {
const config = createMockConfig();
const { rerender } = render(
<UPlotChart config={config} data={validData} width={600} height={400} />,
{ wrapper: Wrapper },
);
const host = hostOf(0);
rerender(
<UPlotChart config={config} data={emptyData} width={600} height={400} />,
);
expect(host.__uplot).toBeUndefined();
});
});
describe('spanGaps data transformation', () => {
it('inserts null break points before passing data to uPlot when a gap exceeds the numeric threshold', () => {
const config = createMockConfig();