2025-08-14 00:24:04 +08:00
|
|
|
import { render, waitFor, fireEvent, act } from 'test/test-utils';
|
2025-04-01 23:49:39 +08:00
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
import { ExpressionQuery, ExpressionQueryType } from '../types';
|
2025-04-01 23:49:39 +08:00
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
import { SqlExpr, SqlExprProps } from './SqlExpr';
|
2025-04-01 23:49:39 +08:00
|
|
|
|
|
|
|
|
jest.mock('@grafana/ui', () => ({
|
|
|
|
|
...jest.requireActual('@grafana/ui'),
|
|
|
|
|
useStyles2: jest.fn().mockImplementation(() => ({})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('@grafana/plugin-ui', () => ({
|
|
|
|
|
SQLEditor: () => <div data-testid="sql-editor">SQL Editor Mock</div>,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
// Mock lazy loaded GenAI components
|
|
|
|
|
jest.mock('./GenAI/GenAISQLSuggestionsButton', () => ({
|
|
|
|
|
GenAISQLSuggestionsButton: ({ currentQuery, initialQuery }: { currentQuery: string; initialQuery: string }) => {
|
|
|
|
|
const text = !currentQuery || currentQuery === initialQuery ? 'Generate suggestion' : 'Improve query';
|
|
|
|
|
return <div data-testid="suggestions-button">{text}</div>;
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('./GenAI/GenAISQLExplainButton', () => ({
|
|
|
|
|
GenAISQLExplainButton: () => <div data-testid="explain-button">Explain query</div>,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Mock custom hooks for GenAI features
|
|
|
|
|
jest.mock('./GenAI/hooks/useSQLSuggestions', () => ({
|
|
|
|
|
useSQLSuggestions: jest.fn(() => ({
|
|
|
|
|
handleApplySuggestion: jest.fn(),
|
|
|
|
|
handleHistoryUpdate: jest.fn(),
|
|
|
|
|
handleCloseDrawer: jest.fn(),
|
|
|
|
|
handleOpenDrawer: jest.fn(),
|
|
|
|
|
isDrawerOpen: false,
|
|
|
|
|
suggestions: [],
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('./GenAI/hooks/useSQLExplanations', () => ({
|
|
|
|
|
useSQLExplanations: jest.fn((currentExpression: string) => ({
|
|
|
|
|
explanation: '',
|
|
|
|
|
handleCloseExplanation: jest.fn(),
|
|
|
|
|
handleOpenExplanation: jest.fn(),
|
|
|
|
|
handleExplain: jest.fn(),
|
|
|
|
|
isExplanationOpen: false,
|
|
|
|
|
shouldShowViewExplanation: false,
|
|
|
|
|
updatePrevExpression: jest.fn(),
|
|
|
|
|
prevExpression: currentExpression,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// Note: Add more mocks if needed for other lazy components
|
|
|
|
|
|
2025-04-01 23:49:39 +08:00
|
|
|
describe('SqlExpr', () => {
|
2025-08-14 00:24:04 +08:00
|
|
|
it('initializes new expressions with default query', async () => {
|
2025-04-01 23:49:39 +08:00
|
|
|
const onChange = jest.fn();
|
|
|
|
|
const refIds = [{ value: 'A' }];
|
|
|
|
|
const query = { refId: 'expr1', type: 'sql', expression: '' } as ExpressionQuery;
|
|
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
await act(async () => {
|
|
|
|
|
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} queries={[]} />);
|
|
|
|
|
});
|
2025-04-01 23:49:39 +08:00
|
|
|
|
|
|
|
|
// Verify onChange was called
|
|
|
|
|
expect(onChange).toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
// Verify essential SQL structure without exact string matching
|
|
|
|
|
const updatedQuery = onChange.mock.calls[0][0];
|
|
|
|
|
expect(updatedQuery.expression.toUpperCase()).toContain('SELECT');
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
it('preserves existing expressions when mounted', async () => {
|
2025-04-01 23:49:39 +08:00
|
|
|
const onChange = jest.fn();
|
|
|
|
|
const refIds = [{ value: 'A' }];
|
|
|
|
|
const existingExpression = 'SELECT 1 AS foo';
|
|
|
|
|
const query = { refId: 'expr1', type: 'sql', expression: existingExpression } as ExpressionQuery;
|
|
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
await act(async () => {
|
|
|
|
|
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} queries={[]} />);
|
|
|
|
|
});
|
2025-04-01 23:49:39 +08:00
|
|
|
|
|
|
|
|
// Check if onChange was called
|
|
|
|
|
if (onChange.mock.calls.length > 0) {
|
|
|
|
|
// If called, ensure it didn't change the expression value
|
|
|
|
|
const updatedQuery = onChange.mock.calls[0][0];
|
|
|
|
|
expect(updatedQuery.expression).toBe(existingExpression);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The SQLEditor should receive the existing expression
|
|
|
|
|
expect(query.expression).toBe(existingExpression);
|
|
|
|
|
});
|
2025-04-02 21:39:36 +08:00
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
it('adds alerting format when alerting prop is true', async () => {
|
2025-04-02 21:39:36 +08:00
|
|
|
const onChange = jest.fn();
|
|
|
|
|
const refIds = [{ value: 'A' }];
|
|
|
|
|
const query = { refId: 'expr1', type: 'sql' } as ExpressionQuery;
|
|
|
|
|
|
2025-08-14 00:24:04 +08:00
|
|
|
await act(async () => {
|
|
|
|
|
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} alerting queries={[]} />);
|
|
|
|
|
});
|
2025-04-02 21:39:36 +08:00
|
|
|
|
|
|
|
|
const updatedQuery = onChange.mock.calls[0][0];
|
|
|
|
|
expect(updatedQuery.format).toBe('alerting');
|
|
|
|
|
});
|
2025-04-01 23:49:39 +08:00
|
|
|
});
|
2025-08-14 00:24:04 +08:00
|
|
|
|
|
|
|
|
describe('SqlExpr with GenAI features', () => {
|
|
|
|
|
const defaultProps: SqlExprProps = {
|
|
|
|
|
onChange: jest.fn(),
|
|
|
|
|
refIds: [{ value: 'A' }],
|
|
|
|
|
query: { refId: 'expression_1', type: ExpressionQueryType.sql, expression: `SELECT * FROM A LIMIT 10` },
|
|
|
|
|
queries: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('renders GenAI buttons with empty expression', async () => {
|
|
|
|
|
const customProps = { ...defaultProps, query: { ...defaultProps.query, expression: '' } };
|
|
|
|
|
const { findByText } = render(<SqlExpr {...customProps} />);
|
|
|
|
|
expect(await findByText('Generate suggestion')).toBeInTheDocument();
|
|
|
|
|
expect(await findByText('Explain query')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders GenAI buttons with non-empty expression', async () => {
|
|
|
|
|
const { findByText } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByText('Improve query')).toBeInTheDocument();
|
|
|
|
|
expect(await findByText('Explain query')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders "Improve query" when currentQuery differs from initialQuery', async () => {
|
|
|
|
|
const customProps = {
|
|
|
|
|
...defaultProps,
|
|
|
|
|
query: { ...defaultProps.query, expression: 'SELECT * FROM A WHERE value > 10' },
|
|
|
|
|
};
|
|
|
|
|
const { findByText } = render(<SqlExpr {...customProps} />);
|
|
|
|
|
expect(await findByText('Improve query')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders View explanation button when shouldShowViewExplanation is true', async () => {
|
|
|
|
|
const { useSQLExplanations } = require('./GenAI/hooks/useSQLExplanations');
|
|
|
|
|
useSQLExplanations.mockImplementation((currentExpression: string) => ({
|
|
|
|
|
shouldShowViewExplanation: true,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { findByText } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByText('View explanation')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders Explain query button when shouldShowViewExplanation is false', async () => {
|
|
|
|
|
const { useSQLExplanations } = require('./GenAI/hooks/useSQLExplanations');
|
|
|
|
|
useSQLExplanations.mockImplementation((currentExpression: string) => ({
|
|
|
|
|
shouldShowViewExplanation: false,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { findByText } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByText('Explain query')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders SuggestionsDrawerButton when there are suggestions', async () => {
|
|
|
|
|
const { useSQLSuggestions } = require('./GenAI/hooks/useSQLSuggestions');
|
|
|
|
|
useSQLSuggestions.mockImplementation(() => ({ suggestions: ['suggestion1', 'suggestion2'] }));
|
|
|
|
|
|
|
|
|
|
const { findByTestId } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByTestId('suggestions-badge')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not render SuggestionsDrawerButton when there are no suggestions', async () => {
|
|
|
|
|
const { useSQLSuggestions } = require('./GenAI/hooks/useSQLSuggestions');
|
|
|
|
|
useSQLSuggestions.mockImplementation(() => ({ suggestions: [] }));
|
|
|
|
|
|
|
|
|
|
const { queryByTestId } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await waitFor(() => queryByTestId('suggestions-badge'))).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('calls handleOpenExplanation when View explanation is clicked', async () => {
|
|
|
|
|
const { useSQLExplanations } = require('./GenAI/hooks/useSQLExplanations');
|
|
|
|
|
const mockHandleOpen = jest.fn();
|
|
|
|
|
useSQLExplanations.mockImplementation(() => ({
|
|
|
|
|
shouldShowViewExplanation: true,
|
|
|
|
|
handleOpenExplanation: mockHandleOpen,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { findByText } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
const button = await findByText('View explanation');
|
|
|
|
|
fireEvent.click(button);
|
|
|
|
|
expect(mockHandleOpen).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders suggestions drawer when isDrawerOpen is true', async () => {
|
|
|
|
|
const { useSQLSuggestions } = require('./GenAI/hooks/useSQLSuggestions');
|
|
|
|
|
useSQLSuggestions.mockImplementation(() => ({
|
|
|
|
|
isDrawerOpen: true,
|
|
|
|
|
suggestions: ['suggestion1', 'suggestion2'],
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const { findByTestId } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByTestId('suggestions-drawer')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders explanation drawer when isExplanationOpen is true', async () => {
|
|
|
|
|
const { useSQLExplanations } = require('./GenAI/hooks/useSQLExplanations');
|
|
|
|
|
useSQLExplanations.mockImplementation(() => ({ isExplanationOpen: true }));
|
|
|
|
|
|
|
|
|
|
const { findByTestId } = render(<SqlExpr {...defaultProps} />);
|
|
|
|
|
expect(await findByTestId('explanation-drawer')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|