* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { ThemeProvider } from '@emotion/react';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18next';
import { LineChart } from '../LineChart';
import * as echarts from 'echarts';
import { binarySearch, useResizeEventDependency } from '../../utils/memoryUtils';
import * as CommonUtils from '../Common';
import '@testing-library/jest-dom';
jest.mock('echarts');
jest.mock('../../utils/memoryUtils');
jest.mock('../Common');
jest.mock('react-i18next', () => ({
...jest.requireActual('react-i18next'),
useTranslation: jest.fn(),
}));
jest.mock('@emotion/react', () => ({
...jest.requireActual('@emotion/react'),
useTheme: jest.fn(),
}));
const mockZr = {
on: jest.fn(),
off: jest.fn(),
};
const mockEChartsInstance = {
setOption: jest.fn(),
dispatchAction: jest.fn(),
on: jest.fn(),
getZr: () => mockZr,
dispose: jest.fn(),
resize: jest.fn(),
};
beforeEach(() => {
(echarts.init as jest.Mock).mockReturnValue(mockEChartsInstance);
(echarts.getInstanceByDom as jest.Mock).mockReturnValue(mockEChartsInstance);
(useResizeEventDependency as jest.Mock).mockReturnValue([0]);
(CommonUtils.useChartCharacter as jest.Mock).mockReturnValue('testCharacter');
});
const mockTheme = {
textColor: '#000000',
};
const mockT = jest.fn((key) => key);
const mockI18n = {
language: 'en-US',
};
const mockUseTranslation = jest.requireMock('react-i18next').useTranslation;
mockUseTranslation.mockReturnValue({
t: mockT,
i18n: mockI18n,
});
const mockUseTheme = jest.requireMock('@emotion/react').useTheme;
mockUseTheme.mockReturnValue(mockTheme);
const mockGraph = {
title: 'Test Chart Title',
columns: ['Time', 'Series 1', 'Series 2', 'Baseline'],
rows: [
['1000', '10', '20'],
['2000', '15', '25'],
['3000', '20', '30'],
],
};
const defaultProps = {
graph: mockGraph,
hAxisTitle: 'Time',
vAxisTitle: 'Value',
isDark: false,
isStatic: false,
};
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<I18nextProvider i18n={i18n}>
<ThemeProvider theme={mockTheme}>
{children}
</ThemeProvider>
</I18nextProvider>
);
describe('LineCharts', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseTranslation.mockReturnValue({
t: mockT,
i18n: mockI18n,
});
mockUseTheme.mockReturnValue(mockTheme);
});
it('should render without crashing', () => {
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
expect(screen.getByText(/Test\s*Chart\s*Title/)).toBeInTheDocument();
expect(screen.getByText(/testCharacter/)).toBeInTheDocument();
});
it('should initialize echarts chart on mount', () => {
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
expect(echarts.init).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
'customed',
{ locale: 'en' },
);
});
it('should initialize with dark theme when isDark is true', () => {
render(
<TestWrapper>
<LineChart {...defaultProps} isDark={true} />
</TestWrapper>,
);
expect(echarts.init).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
'dark',
{ locale: 'en' },
);
});
it('should set chart options correctly', async () => {
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.setOption).toHaveBeenCalled();
});
const setOptionCall = mockEChartsInstance.setOption.mock.calls[0][0];
expect(setOptionCall).toHaveProperty('series');
expect(setOptionCall.series).toHaveLength(3);
});
it('should handle dataZoom event', async () => {
const mockOnSelectionChanged = jest.fn();
render(
<TestWrapper>
<LineChart {...defaultProps} onSelectionChanged={mockOnSelectionChanged} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.on).toHaveBeenCalledWith('dataZoom', expect.any(Function));
});
const dataZoomHandler = mockEChartsInstance.on.mock.calls.find(
call => call[0] === 'dataZoom',
)[1];
const mockEvent = { batch: [{ startValue: 1000, endValue: 2000 }] };
dataZoomHandler(mockEvent);
expect(mockOnSelectionChanged).toHaveBeenCalledWith(1000, 2000);
});
it('should handle restore event', async () => {
const mockOnSelectionChanged = jest.fn();
render(
<TestWrapper>
<LineChart {...defaultProps} onSelectionChanged={mockOnSelectionChanged} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.on).toHaveBeenCalledWith('restore', expect.any(Function));
});
const restoreHandler = mockEChartsInstance.on.mock.calls.find(
call => call[0] === 'restore',
)[1];
restoreHandler();
expect(mockOnSelectionChanged).toHaveBeenCalledWith(0, -1);
});
it('should handle click event for point selection', async () => {
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.on).toHaveBeenCalledWith('click', expect.any(Function));
});
const clickHandler = mockEChartsInstance.on.mock.calls.find(
call => call[0] === 'click',
)[1];
const mockClickEvent = {
seriesId: 'series-1',
dataIndex: 2,
};
clickHandler(mockClickEvent);
expect(mockEChartsInstance.dispatchAction).toHaveBeenCalledWith({
type: 'unselect',
seriesId: 'series-1',
dataIndex: [],
});
expect(mockEChartsInstance.dispatchAction).toHaveBeenCalledWith({
type: 'select',
seriesId: 'series-1',
dataIndex: 2,
});
});
it('should handle contextmenu event', async () => {
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
const zrOnCall = mockEChartsInstance.getZr().on.mock.calls.find(
call => call[0] === 'contextmenu',
);
expect(zrOnCall).toBeDefined();
});
it('should highlight points when record is provided', async () => {
const mockRecord = {
allocationTime: '1500',
releaseTime: '2500',
};
(binarySearch as jest.Mock)
.mockReturnValueOnce(1)
.mockReturnValueOnce(2);
render(
<TestWrapper>
<LineChart {...defaultProps} record={mockRecord} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.dispatchAction).toHaveBeenCalledWith({
type: 'highlight',
seriesName: undefined,
dataIndex: [1, 2],
});
});
});
it('should downplay points when record is not provided', async () => {
render(
<TestWrapper>
<LineChart {...defaultProps} record={undefined} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.dispatchAction).toHaveBeenCalledWith({
type: 'downplay',
seriesName: undefined,
dataIndex: [],
});
});
});
it('should handle resize events', () => {
const mockResizeDependency = ['resize-event'];
(useResizeEventDependency as jest.Mock).mockReturnValue([mockResizeDependency]);
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
expect(echarts.getInstanceByDom).toHaveBeenCalled();
expect(mockEChartsInstance.resize).toHaveBeenCalled();
});
it('should not render title when graph title is empty', () => {
const propsWithoutTitle = {
...defaultProps,
graph: { ...mockGraph, title: '' },
};
render(
<TestWrapper>
<LineChart {...propsWithoutTitle} />
</TestWrapper>,
);
expect(screen.queryByText('Test Chart Title')).not.toBeInTheDocument();
});
it('should handle binary search edge cases', async () => {
const mockRecord = {
allocationTime: '500',
releaseTime: '4000',
};
(binarySearch as jest.Mock)
.mockReturnValueOnce(-1)
.mockReturnValueOnce(-1);
render(
<TestWrapper>
<LineChart {...defaultProps} record={mockRecord} />
</TestWrapper>,
);
await waitFor(() => {
expect(mockEChartsInstance.dispatchAction).toHaveBeenCalledWith({
type: 'downplay',
seriesName: undefined,
dataIndex: [],
});
});
});
it('should clean up chart on unmount', () => {
const { unmount } = render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
unmount();
expect(mockEChartsInstance.dispose).toHaveBeenCalled();
});
it('should handle different languages', () => {
mockUseTranslation.mockReturnValue({
t: mockT,
i18n: { language: 'zh-CN' },
});
render(
<TestWrapper>
<LineChart {...defaultProps} />
</TestWrapper>,
);
expect(echarts.init).toHaveBeenCalledWith(
expect.any(HTMLDivElement),
'customed',
{ locale: 'zh' },
);
});
});