import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { select } from 'react-select-event';
import { LogRowModel, dateTime } from '@grafana/data';
import { LogContextProvider, SHOULD_INCLUDE_PIPELINE_OPERATIONS } from '../LogContextProvider';
import { ContextFilter, LokiQuery } from '../types';
import { IS_LOKI_LOG_CONTEXT_UI_OPEN, LokiContextUi, LokiContextUiProps } from './LokiContextUi';
// we have to mock out reportInteraction, otherwise it crashes the test.
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
reportInteraction: () => null,
}));
const setupProps = (): LokiContextUiProps => {
const defaults: LokiContextUiProps = {
logContextProvider: Object.assign({}, mockLogContextProvider) as unknown as LogContextProvider,
updateFilter: jest.fn(),
row: {
entry: 'WARN test 1.23 on [xxx]',
labels: {
label1: 'value1',
label3: 'value3',
},
timeEpochMs: new Date().getTime(),
} as unknown as LogRowModel,
onClose: jest.fn(),
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
runContextQuery: jest.fn(),
};
return defaults;
};
const mockLogContextProvider = {
getInitContextFilters: jest.fn().mockImplementation(() =>
Promise.resolve({
contextFilters: [
{ value: 'value1', enabled: true, nonIndexed: false, label: 'label1' },
{ value: 'value3', enabled: false, nonIndexed: true, label: 'label3' },
],
preservedFiltersApplied: false,
})
),
processContextFiltersToExpr: jest.fn().mockImplementation(
(contextFilters: ContextFilter[], query: LokiQuery | undefined) =>
`{${contextFilters
.filter((filter) => filter.enabled)
.map((filter) => `${filter.label}="${filter.value}"`)
.join('` ')}}`
),
processPipelineStagesToExpr: jest
.fn()
.mockImplementation((currentExpr: string, query: LokiQuery | undefined) => `${currentExpr} | newOperation`),
getLogRowContext: jest.fn(),
queryContainsValidPipelineStages: jest.fn().mockReturnValue(true),
prepareExpression: jest.fn().mockImplementation(
(contextFilters: ContextFilter[], query: LokiQuery | undefined) =>
`{${contextFilters
.filter((filter) => filter.enabled)
.map((filter) => `${filter.label}="${filter.value}"`)
.join('` ')}}`
),
};
describe('LokiContextUi', () => {
const savedGlobal = global;
beforeAll(() => {
// TODO: `structuredClone` is not yet in jsdom https://github.com/jsdom/jsdom/issues/3363
if (!global.structuredClone) {
global.structuredClone = function structuredClone(objectToClone: unknown) {
const stringified = JSON.stringify(objectToClone);
const parsed = JSON.parse(stringified);
return parsed;
};
}
});
afterAll(() => {
global = savedGlobal;
});
beforeEach(() => {
window.localStorage.setItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS, 'true');
window.localStorage.setItem(IS_LOKI_LOG_CONTEXT_UI_OPEN, 'true');
});
afterEach(() => {
window.localStorage.clear();
});
it('renders and shows executed query text', async () => {
const props = setupProps();
render();
await waitFor(() => {
// We should see the query text (it is split into multiple spans)
expect(screen.getByText('{')).toBeInTheDocument();
expect(screen.getByText('label1')).toBeInTheDocument();
expect(screen.getByText('=')).toBeInTheDocument();
expect(screen.getByText('"value1"')).toBeInTheDocument();
expect(screen.getByText('}')).toBeInTheDocument();
});
});
it('initialize context filters', async () => {
const props = setupProps();
render();
await waitFor(() => {
expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalled();
});
});
it('calls `getInitContextFilters` with the right set of parameters', async () => {
const props = setupProps();
render();
await waitFor(() => {
expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalledWith(props.row, props.origQuery, {
from: dateTime(props.row.timeEpochMs),
to: dateTime(props.row.timeEpochMs),
raw: { from: dateTime(props.row.timeEpochMs), to: dateTime(props.row.timeEpochMs) },
});
});
});
it('finds label1 as a real label', async () => {
const props = setupProps();
render();
await waitFor(() => {
expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalled();
});
const selects = await screen.findAllByRole('combobox');
await select(selects[0], 'label1="value1"', { container: document.body });
});
it('finds label3 as a parsed label', async () => {
const props = setupProps();
render();
await waitFor(() => {
expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalled();
});
const selects = await screen.findAllByRole('combobox');
await select(selects[1], 'label3="value3"', { container: document.body });
});
it('calls updateFilter when selecting a label', async () => {
jest.useFakeTimers();
const props = setupProps();
render();
await waitFor(() => {
expect(props.logContextProvider.getInitContextFilters).toHaveBeenCalled();
expect(screen.getAllByRole('combobox')).toHaveLength(2);
});
await select(screen.getAllByRole('combobox')[1], 'label3="value3"', { container: document.body });
act(() => {
jest.runAllTimers();
});
expect(props.updateFilter).toHaveBeenCalled();
jest.useRealTimers();
});
it('unmounts and calls onClose', async () => {
const props = setupProps();
const comp = render();
comp.unmount();
await waitFor(() => {
expect(props.onClose).toHaveBeenCalled();
});
});
it('displays executed query even if context ui closed', async () => {
const props = setupProps();
render();
// We start with the context ui open and click on it to close
await userEvent.click(screen.getAllByRole('button')[0]);
await waitFor(() => {
// We should see the query text (it is split into multiple spans)
expect(screen.getByText('{')).toBeInTheDocument();
expect(screen.getByText('label1')).toBeInTheDocument();
expect(screen.getByText('=')).toBeInTheDocument();
expect(screen.getByText('"value1"')).toBeInTheDocument();
expect(screen.getByText('}')).toBeInTheDocument();
});
});
it('does not show parsed labels section if origQuery has 0 parsers', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"}',
refId: 'A',
},
};
render();
await waitFor(() => {
expect(screen.queryByText('Refine the search')).not.toBeInTheDocument();
});
});
it('shows parsed labels section if origQuery has 1 parser', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
render();
await waitFor(() => {
expect(screen.getByText('Refine the search')).toBeInTheDocument();
});
});
it('renders pipeline operations switch as enabled when saved in localstorage', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
window.localStorage.setItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS, 'true');
render();
await waitFor(() => {
expect((screen.getByRole('switch') as HTMLInputElement).checked).toBe(true);
});
});
it('renders pipeline operations switch as disabled when saved in localstorage', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
window.localStorage.setItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS, 'false');
render();
await waitFor(() => {
expect((screen.getByRole('switch') as HTMLInputElement).checked).toBe(false);
});
});
it('renders pipeline operations switch if query contains valid pipeline stages', async () => {
const props = setupProps();
(props.logContextProvider.queryContainsValidPipelineStages as jest.Mock).mockReturnValue(true);
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
window.localStorage.setItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS, 'true');
render();
await waitFor(() => {
expect(screen.getByRole('switch')).toBeInTheDocument();
});
});
it('does not render pipeline operations switch if query does not contain valid pipeline stages', async () => {
const props = setupProps();
(props.logContextProvider.queryContainsValidPipelineStages as jest.Mock).mockReturnValue(false);
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
window.localStorage.setItem(SHOULD_INCLUDE_PIPELINE_OPERATIONS, 'true');
render();
await waitFor(() => {
expect(screen.queryByRole('switch')).toBeNull();
});
});
it('does not show parsed labels section if origQuery has 2 parsers', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt | json',
refId: 'A',
},
};
render();
await waitFor(() => {
expect(screen.queryByText('Refine the search')).not.toBeInTheDocument();
});
});
it('should revert to original query when revert button clicked', async () => {
const props = setupProps();
const newProps = {
...props,
origQuery: {
expr: '{label1="value1"} | logfmt',
refId: 'A',
},
};
render();
// In initial query, label3 is not selected
await waitFor(() => {
expect(screen.queryByText('label3="value3"')).not.toBeInTheDocument();
});
// We select parsed label and label3="value3" should appear
const parsedLabelsInput = screen.getAllByRole('combobox')[1];
await userEvent.click(parsedLabelsInput);
await userEvent.type(parsedLabelsInput, '{enter}');
expect(screen.getByText('label3="value3"')).toBeInTheDocument();
// We click on revert button and label3="value3" should disappear
const revertButton = screen.getByTestId('revert-button');
await userEvent.click(revertButton);
await waitFor(() => {
expect(screen.queryByText('label3="value3"')).not.toBeInTheDocument();
});
});
it('shows if preserved filters are applied', async () => {
const props = setupProps();
const newProps = {
...props,
logContextProvider: {
...props.logContextProvider,
getInitContextFilters: jest.fn().mockImplementation(() =>
Promise.resolve({
contextFilters: [
{ value: 'value1', enabled: true, nonIndexed: false, label: 'label1' },
{ value: 'value3', enabled: false, nonIndexed: true, label: 'label3' },
],
preservedFiltersApplied: true,
})
),
},
} as unknown as LokiContextUiProps;
render();
expect(await screen.findByText('Previously used filters have been applied.')).toBeInTheDocument();
});
it('does not shows if preserved filters are not applied', async () => {
// setupProps() already has preservedFiltersApplied: false
const props = setupProps();
render();
await waitFor(() => {
expect(screen.queryByText('Previously used filters have been applied.')).not.toBeInTheDocument();
});
});
});