grafana_bak/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx
2025-04-01 10:38:02 +09:00

316 lines
12 KiB
TypeScript

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, LogSortOrderChangeEvent, LogsSortOrder, store } from '@grafana/data';
import { config, getAppEvents } from '@grafana/runtime';
import { LokiQuery, LokiQueryDirection, LokiQueryType } from '../../types';
import { LokiQueryBuilderOptions, Props } from './LokiQueryBuilderOptions';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
featureToggles: {
...jest.requireActual('@grafana/runtime').featureToggles,
lokiShardSplitting: true,
},
},
getAppEvents: jest.fn(),
}));
const subscribeMock = jest.fn();
beforeAll(() => {
config.featureToggles.lokiShardSplitting = true;
subscribeMock.mockImplementation(() => ({ unsubscribe: jest.fn() }));
jest.mocked(getAppEvents).mockReturnValue({
publish: jest.fn(),
getStream: jest.fn(),
subscribe: subscribeMock,
removeAllListeners: jest.fn(),
newScopedBus: jest.fn(),
});
});
describe('LokiQueryBuilderOptions', () => {
it('can change query type', async () => {
const { props } = setup();
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByLabelText('Range')).toBeChecked();
await userEvent.click(screen.getByLabelText('Instant'));
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
queryType: LokiQueryType.Instant,
});
});
it('can change legend format', async () => {
const { props } = setup();
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
// First autosize input is a Legend
const element = screen.getAllByTestId('autosize-input')[0];
await userEvent.type(element, 'asd');
await userEvent.keyboard('{enter}');
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
legendFormat: 'asd',
});
});
it('can change line limit to valid value', async () => {
const { props } = setup({ expr: '{foo="bar"}' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
// Second autosize input is a Line limit
const element = screen.getAllByTestId('autosize-input')[1];
await userEvent.type(element, '10');
await userEvent.keyboard('{enter}');
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
maxLines: 10,
});
});
it('does not change line limit to invalid numeric value', async () => {
const { props } = setup({ expr: '{foo="bar"}' });
// We need to start with some value to be able to change it
props.query.maxLines = 10;
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
// Second autosize input is a Line limit
const element = screen.getAllByTestId('autosize-input')[1];
await userEvent.type(element, '-10');
await userEvent.keyboard('{enter}');
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
maxLines: undefined,
});
});
it('does not change line limit to invalid text value', async () => {
const { props } = setup({ expr: '{foo="bar"}' });
// We need to start with some value to be able to change it
props.query.maxLines = 10;
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
// Second autosize input is a Line limit
const element = screen.getAllByTestId('autosize-input')[1];
await userEvent.type(element, 'asd');
await userEvent.keyboard('{enter}');
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
maxLines: undefined,
});
});
it('shows correct options for log query', async () => {
setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Backward });
expect(screen.getByText('Line limit: 20')).toBeInTheDocument();
expect(screen.getByText('Type: Range')).toBeInTheDocument();
expect(screen.getByText('Direction: Backward')).toBeInTheDocument();
expect(screen.queryByText(/step/i)).not.toBeInTheDocument();
});
it('shows correct options for metric query', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1m', resolution: 2 });
expect(screen.queryByText('Line limit: 20')).not.toBeInTheDocument();
expect(screen.getByText('Type: Range')).toBeInTheDocument();
expect(screen.getByText('Step: 1m')).toBeInTheDocument();
expect(screen.getByText('Resolution: 1/2')).toBeInTheDocument();
expect(screen.queryByText(/Direction/)).not.toBeInTheDocument();
});
it('does not show resolution field if resolution is not set', async () => {
setup({ expr: 'rate({foo="bar"}[5m]' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText('Resolution')).not.toBeInTheDocument();
});
it('does not show resolution field if resolution is set to default value 1', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', resolution: 1 });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText('Resolution')).not.toBeInTheDocument();
});
it('does shows resolution field with warning if resolution is set to non-default value', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', resolution: 2 });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText('Resolution')).toBeInTheDocument();
expect(
screen.getByText("The 'Resolution' is deprecated. Use 'Step' editor instead to change step parameter.")
).toBeInTheDocument();
});
it.each(['abc', 10])('shows correct options for metric query with invalid step', async (step: string | number) => {
// @ts-expect-error Expected for backward compatibility test
setup({ expr: 'rate({foo="bar"}[5m]', step });
expect(screen.queryByText('Line limit: 20')).not.toBeInTheDocument();
expect(screen.getByText('Type: Range')).toBeInTheDocument();
expect(screen.getByText('Step: Invalid value')).toBeInTheDocument();
});
it('shows error when invalid value in step', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: 'a' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByText(/Invalid step/)).toBeInTheDocument();
});
it('does not show error when valid value in step', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1m' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('does not show error when valid millisecond value in step', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1ms' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('does not show error when valid day value in step', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '1d' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Invalid step/)).not.toBeInTheDocument();
});
it('does not show instant type when using a log query', async () => {
setup({ expr: '{foo="bar"}', queryType: LokiQueryType.Instant });
expect(screen.queryByText(/Instant/)).not.toBeInTheDocument();
});
it('does not show instant type in the options when using a log query', async () => {
setup({ expr: '{foo="bar"}', step: '1m' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.queryByText(/Instant/)).not.toBeInTheDocument();
});
it('allows to clear step input', async () => {
setup({ expr: 'rate({foo="bar"}[5m]', step: '4s' });
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(screen.getByDisplayValue('4s')).toBeInTheDocument();
await userEvent.clear(screen.getByDisplayValue('4s'));
expect(screen.queryByDisplayValue('4s')).not.toBeInTheDocument();
});
it('should transform non duration numbers to duration', async () => {
const onChange = jest.fn();
setup({ expr: 'rate({foo="bar"}[5m]', step: '4' }, onChange);
await userEvent.click(screen.getByRole('button', { name: /Options/ }));
expect(onChange).toHaveBeenCalledWith({
refId: 'A',
expr: 'rate({foo="bar"}[5m]',
step: '4s',
});
});
describe('Query direction', () => {
it("initializes query direction when it's empty in Explore or Dashboards", () => {
const onChange = jest.fn();
setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
expect(onChange).toHaveBeenCalledWith({
expr: '{foo="bar"}',
refId: 'A',
direction: LokiQueryDirection.Backward,
});
});
it('does not change direction on initialization elsewhere', () => {
const onChange = jest.fn();
setup({ expr: '{foo="bar"}' }, onChange, { app: undefined });
expect(onChange).not.toHaveBeenCalled();
});
it('uses backward as default in Explore with no previous stored preference', () => {
const onChange = jest.fn();
store.delete('grafana.explore.logs.sortOrder');
setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
expect(onChange).toHaveBeenCalledWith({
expr: '{foo="bar"}',
refId: 'A',
direction: LokiQueryDirection.Backward,
});
});
it('uses the stored sorting option to determine direction in Explore', () => {
store.set('grafana.explore.logs.sortOrder', LogsSortOrder.Ascending);
const onChange = jest.fn();
setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore });
expect(onChange).toHaveBeenCalledWith({
expr: '{foo="bar"}',
refId: 'A',
direction: LokiQueryDirection.Forward,
});
store.delete('grafana.explore.logs.sortOrder');
});
describe('Event handling', () => {
let listener: (event: LogSortOrderChangeEvent) => void = jest.fn();
const onChangeMock = jest.fn();
beforeEach(() => {
onChangeMock.mockClear();
listener = jest.fn();
subscribeMock.mockImplementation((_: unknown, callback: (event: LogSortOrderChangeEvent) => void) => {
listener = callback;
return { unsubscribe: jest.fn() };
});
});
it('subscribes to sort change event and updates the direction', () => {
setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Backward }, onChangeMock, {
app: CoreApp.Dashboard,
});
expect(screen.getByText(/Direction: Backward/)).toBeInTheDocument();
listener(
new LogSortOrderChangeEvent({
order: LogsSortOrder.Ascending,
})
);
expect(onChangeMock).toHaveBeenCalledTimes(1);
expect(onChangeMock).toHaveBeenCalledWith({
direction: 'forward',
expr: '{foo="bar"}',
refId: 'A',
});
});
it('does not change the direction when the current direction is scan', () => {
setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Scan }, onChangeMock, { app: CoreApp.Dashboard });
expect(screen.getByText(/Direction: Scan/)).toBeInTheDocument();
listener(
new LogSortOrderChangeEvent({
order: LogsSortOrder.Ascending,
})
);
expect(onChangeMock).not.toHaveBeenCalled();
});
});
});
});
function setup(queryOverrides: Partial<LokiQuery> = {}, onChange = jest.fn(), propOverrides: Partial<Props> = {}) {
const props = {
query: {
refId: 'A',
expr: '',
...queryOverrides,
},
onRunQuery: jest.fn(),
onChange,
maxLines: 20,
queryStats: { streams: 0, chunks: 0, bytes: 0, entries: 0 },
...propOverrides,
};
const { container } = render(<LokiQueryBuilderOptions {...props} />);
return { container, props };
}