grafana/public/app/features/expressions/components/SqlExpr.test.tsx

207 lines
7.9 KiB
TypeScript

import { render, waitFor, fireEvent, act } from 'test/test-utils';
import { ExpressionQuery, ExpressionQueryType } from '../types';
import { SqlExpr, SqlExprProps } from './SqlExpr';
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>,
}));
// 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
describe('SqlExpr', () => {
it('initializes new expressions with default query', async () => {
const onChange = jest.fn();
const refIds = [{ value: 'A' }];
const query = { refId: 'expr1', type: 'sql', expression: '' } as ExpressionQuery;
await act(async () => {
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} queries={[]} />);
});
// 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');
});
it('preserves existing expressions when mounted', async () => {
const onChange = jest.fn();
const refIds = [{ value: 'A' }];
const existingExpression = 'SELECT 1 AS foo';
const query = { refId: 'expr1', type: 'sql', expression: existingExpression } as ExpressionQuery;
await act(async () => {
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} queries={[]} />);
});
// 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);
});
it('adds alerting format when alerting prop is true', async () => {
const onChange = jest.fn();
const refIds = [{ value: 'A' }];
const query = { refId: 'expr1', type: 'sql' } as ExpressionQuery;
await act(async () => {
render(<SqlExpr onChange={onChange} refIds={refIds} query={query} alerting queries={[]} />);
});
const updatedQuery = onChange.mock.calls[0][0];
expect(updatedQuery.format).toBe('alerting');
});
});
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();
});
});