Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/plugins/gravity-charts/__tests__/Base.visual.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ describe('GravityCharts base tests', () => {
settings.set({plugins: [GravityChartsPlugin]});
});

test('should render chart with valid data', async () => {
// TODO: flaky — screenshot captured before async chart render. Skipped until fixed.
// https://github.com/gravity-ui/chartkit/issues/896
test.skip('should render chart with valid data', async () => {
const data: ChartData = {
series: {
data: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const SPLIT_TOOLTIP_DATA: ChartData = {
xAxis: {type: 'category', categories: ['A', 'B']},
};

describe('Split tooltip visual tests', () => {
// TODO: flaky — screenshots captured before async chart render. Skipped until fixed.
// https://github.com/gravity-ui/chartkit/issues/896
describe.skip('Split tooltip visual tests', () => {
beforeAll(() => {
settings.set({plugins: [GravityChartsPlugin]});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Regression tests for the custom tooltip formatter override (libraryConfig.tooltip.formatter).
//
// Background: prepareConfig used to `delete options.highcharts.tooltip.formatter`, mutating the
// caller's libraryConfig. The override then worked only on the FIRST config build; any later
// build reusing the same libraryConfig reference fell back to the default ChartKit tooltip.
// React 18+ StrictMode double-mounts components in dev (mount -> unmount -> remount), which is
// exactly such a "second build with the same config" — so the override silently broke after a
// React 17 -> 19 upgrade.
//
// These run in the browser (visual) project because prepareConfig / HighchartsComponent need `window`.

import React, {StrictMode} from 'react';

import {render} from '../../../../test-utils/utils';
import {ChartKit} from '../../../components/ChartKit';
import {settings} from '../../../libs';
import {HighchartsPlugin} from '../index';
import {data as lineMock} from '../mocks/line';
import {prepareConfig} from '../renderer/helpers/config/config';

settings.set({lang: 'en'});

const CUSTOM_TOOLTIP = 'CUSTOM-TOOLTIP-OVERRIDE';

function callTooltipFormatter(config: {tooltip: {formatter: (tooltip: unknown) => string}}) {
const fakeThis = {series: {type: 'line'}};
const fakeTooltip = {
splitTooltip: false,
chart: {options: {chart: {type: 'line'}}, userOptions: {_getComments: () => []}},
defaultFormatter: () => ['DEFAULT'],
};
return config.tooltip.formatter.call(fakeThis, fakeTooltip);
}

describe('highcharts custom tooltip formatter override', () => {
test('prepareConfig: survives repeated builds and does not mutate libraryConfig', () => {
const libraryConfig = {
chart: {type: 'line'},
tooltip: {formatter: () => CUSTOM_TOOLTIP},
};

const first = prepareConfig(lineMock.data, {highcharts: libraryConfig}) as ReturnType<
typeof prepareConfig
> & {tooltip: {formatter: (tooltip: unknown) => string}};
const second = prepareConfig(lineMock.data, {highcharts: libraryConfig}) as typeof first;

expect(callTooltipFormatter(first)).toContain(CUSTOM_TOOLTIP);
// Regression: the second build with the SAME object used to fall back to the default tooltip.
expect(callTooltipFormatter(second)).toContain(CUSTOM_TOOLTIP);
// The caller's libraryConfig must not be mutated.
expect(typeof libraryConfig.tooltip.formatter).toBe('function');
});

test('component: rendering under StrictMode does not consume tooltip.formatter', async () => {
settings.set({plugins: [HighchartsPlugin]});

// The same reference is reused across StrictMode's mount -> unmount -> remount cycle,
// mirroring a module-level libraryConfig shared between renders.
const sharedLibraryConfig = {
chart: {type: 'line'},
tooltip: {formatter: () => CUSTOM_TOOLTIP},
};
const chartData = {data: lineMock.data, libraryConfig: sharedLibraryConfig};

await render(
<StrictMode>
<div style={{width: 600, height: 400}}>
<ChartKit
id="hc-tooltip-formatter"
type="highcharts"
data={chartData as never}
/>
</div>
</StrictMode>,
);

// Wait until HighchartsComponent has mounted (getDerivedStateFromProps -> prepareConfig ran).
await vi.waitFor(() => {
expect(document.querySelector('.chartkit-graph')).toBeTruthy();
});

// Root cause: the shared libraryConfig must still carry the formatter after rendering.
expect(typeof sharedLibraryConfig.tooltip.formatter).toBe('function');
});
});
9 changes: 8 additions & 1 deletion src/plugins/highcharts/renderer/helpers/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -1828,7 +1828,8 @@ export function prepareConfig(data, options, isMobile, holidays) {
params.tooltip.formatter = function (tooltip) {
return `<div class="${b()}">${formatter.call(this, tooltip)}</div>`;
};
delete options.highcharts.tooltip.formatter;
// The raw formatter is stripped from the merge source (preparedHighchartsOptions) below,
// not from options.highcharts, so the caller's libraryConfig is not mutated.
} else {
params.tooltip.formatter = function (tooltip) {
const serieType =
Expand Down Expand Up @@ -1926,6 +1927,12 @@ export function prepareConfig(data, options, isMobile, holidays) {
options.highcharts,
);

// params.tooltip.formatter already holds the wrapped user formatter (see above).
// Drop the raw formatter from this clone so the merge below doesn't override the wrapper.
if (preparedHighchartsOptions.tooltip) {
delete preparedHighchartsOptions.tooltip.formatter;
}

mergeWith(params, getTypeParams(data, options), preparedHighchartsOptions, (a, b) => {
if (typeof a === 'function' && typeof b === 'function' && a !== b) {
return function (event, ...args) {
Expand Down
Loading