The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/logs/components/panel/LogList.test.tsx

341 lines
13 KiB

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, getDefaultTimeRange, LogRowModel, LogsDedupStrategy, LogsSortOrder, store } from '@grafana/data';
import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils';
import { createLogRow } from '../mocks/logRow';
import { LogList, Props } from './LogList';
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
config: {
...jest.requireActual('@grafana/runtime').config,
featureToggles: {
...jest.requireActual('@grafana/runtime').config.featureToggles,
logRowsPopoverMenu: true,
},
},
};
});
jest.mock('../../utils', () => ({
...jest.requireActual('../../utils'),
isPopoverMenuDisabled: jest.fn(),
disablePopoverMenu: jest.fn(),
enablePopoverMenu: jest.fn(),
}));
describe('LogList', () => {
let logs: LogRowModel[], defaultProps: Props;
beforeEach(() => {
logs = [
createLogRow({ uid: '1', labels: { name_of_the_label: 'value of the label' } }),
createLogRow({ uid: '2' }),
];
defaultProps = {
app: CoreApp.Explore,
containerElement: document.createElement('div'),
dedupStrategy: LogsDedupStrategy.none,
displayedFields: [],
enableLogDetails: true,
logs,
showControls: false,
showTime: false,
sortOrder: LogsSortOrder.Descending,
timeRange: getDefaultTimeRange(),
timeZone: 'browser',
wrapLogMessage: false,
};
});
test('Renders a list of logs without controls ', async () => {
render(<LogList {...defaultProps} />);
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('log message 2')).toBeInTheDocument();
expect(screen.queryByLabelText('Scroll to bottom')).not.toBeInTheDocument();
});
test('Renders a list of logs with controls', async () => {
render(<LogList {...defaultProps} showControls={true} />);
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('log message 2')).toBeInTheDocument();
expect(screen.getByLabelText('Scroll to bottom')).toBeInTheDocument();
});
test('Reports mouse over events', async () => {
const onLogRowHover = jest.fn();
render(<LogList {...defaultProps} onLogLineHover={onLogRowHover} />);
await userEvent.hover(screen.getByText('log message 1'));
expect(onLogRowHover).toHaveBeenCalledTimes(1);
expect(onLogRowHover).toHaveBeenCalledWith(expect.objectContaining(logs[0]));
});
test('Supports showing log details', async () => {
jest.spyOn(store, 'get').mockImplementation((option: string) => {
if (option === 'storage-key.detailsMode') {
return 'sidebar';
}
return undefined;
});
const onClickFilterLabel = jest.fn();
const onClickFilterOutLabel = jest.fn();
const onClickShowField = jest.fn();
render(
<LogList
{...defaultProps}
enableLogDetails={true}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowField={onClickShowField}
logOptionsStorageKey="storage-key"
/>
);
await userEvent.click(screen.getByText('log message 1'));
await screen.findByText('Fields');
expect(screen.getByText('name_of_the_label')).toBeInTheDocument();
expect(screen.getByText('value of the label')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Filter for value in query A'));
expect(onClickFilterLabel).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Filter out value in query A'));
expect(onClickFilterOutLabel).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Show this field instead of the message'));
expect(onClickShowField).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Close log details'));
expect(screen.queryByText('Fields')).not.toBeInTheDocument();
expect(screen.queryByText('Close log details')).not.toBeInTheDocument();
});
test('Supports showing inline log details', async () => {
jest.spyOn(store, 'get').mockImplementation((option: string) => {
if (option === 'storage-key.detailsMode') {
return 'inline';
}
return undefined;
});
const onClickFilterLabel = jest.fn();
const onClickFilterOutLabel = jest.fn();
const onClickShowField = jest.fn();
render(
<LogList
{...defaultProps}
enableLogDetails={true}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowField={onClickShowField}
logOptionsStorageKey="storage-key"
/>
);
await userEvent.click(screen.getByText('log message 1'));
await screen.findByText('Fields');
expect(screen.getByText('name_of_the_label')).toBeInTheDocument();
expect(screen.getByText('value of the label')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Filter for value in query A'));
expect(onClickFilterLabel).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Filter out value in query A'));
expect(onClickFilterOutLabel).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Show this field instead of the message'));
expect(onClickShowField).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByLabelText('Close log details'));
expect(screen.queryByText('Fields')).not.toBeInTheDocument();
expect(screen.queryByText('Close log details')).not.toBeInTheDocument();
});
test('Allows people to select text without opening log details', async () => {
const spy = jest.spyOn(document, 'getSelection');
spy.mockReturnValue({
toString: () => 'selected log line',
removeAllRanges: () => {},
addRange: (range: Range) => {},
} as Selection);
render(<LogList {...defaultProps} enableLogDetails={true} />);
await userEvent.click(screen.getByText('log message 1'));
expect(screen.queryByText('name_of_the_label')).not.toBeInTheDocument();
expect(screen.queryByText('value of the label')).not.toBeInTheDocument();
expect(screen.queryByText('Fields')).not.toBeInTheDocument();
expect(screen.queryByText('Close log details')).not.toBeInTheDocument();
spy.mockRestore();
});
describe('Popover menu', () => {
function setup(overrides: Partial<Props> = {}) {
return render(
<LogList {...defaultProps} onClickFilterString={jest.fn()} onClickFilterOutString={jest.fn()} {...overrides} />
);
}
let orgGetSelection: () => Selection | null;
beforeEach(() => {
jest.mocked(isPopoverMenuDisabled).mockReturnValue(false);
});
beforeAll(() => {
orgGetSelection = document.getSelection;
jest.spyOn(document, 'getSelection').mockReturnValue({
toString: () => 'selected log line',
removeAllRanges: () => {},
addRange: (range: Range) => {},
} as Selection);
});
afterAll(() => {
document.getSelection = orgGetSelection;
});
it('Does not appear in the document', () => {
setup();
expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
});
it('Appears after selecting text', async () => {
setup();
await userEvent.click(screen.getByText('log message 1'));
expect(screen.getByText('Copy selection')).toBeInTheDocument();
expect(screen.getByText('Add as line contains filter')).toBeInTheDocument();
expect(screen.getByText('Add as line does not contain filter')).toBeInTheDocument();
});
it('Can be disabled', async () => {
setup();
await userEvent.click(screen.getByText('log message 1'));
await userEvent.click(screen.getByText('Disable menu'));
await userEvent.click(screen.getByText('Confirm'));
expect(disablePopoverMenu).toHaveBeenCalledTimes(1);
});
it('Does not appear when disabled', async () => {
jest.mocked(isPopoverMenuDisabled).mockReturnValue(true);
setup();
await userEvent.click(screen.getByText('log message 1'));
expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
});
it('Can be re-enabled', async () => {
jest.mocked(isPopoverMenuDisabled).mockReturnValue(true);
const user = userEvent.setup();
setup();
await user.keyboard('[AltLeft>]'); // Press Alt (without releasing it)
await user.click(screen.getByText('log message 1'));
expect(enablePopoverMenu).toHaveBeenCalledTimes(1);
});
it('Does not appear when the props are not defined', async () => {
setup({
onClickFilterOutString: undefined,
onClickFilterString: undefined,
});
await userEvent.click(screen.getByText('log message 1'));
expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
});
it('Appears after selecting text', async () => {
const onClickFilterOutString = jest.fn();
const onClickFilterString = jest.fn();
setup({
onClickFilterOutString,
onClickFilterString,
});
await userEvent.click(screen.getByText('log message 1'));
expect(screen.getByText('Copy selection')).toBeInTheDocument();
await userEvent.click(screen.getByText('Add as line contains filter'));
await userEvent.click(screen.getByText('log message 1'));
expect(screen.getByText('Copy selection')).toBeInTheDocument();
await userEvent.click(screen.getByText('Add as line does not contain filter'));
expect(onClickFilterOutString).toHaveBeenCalledTimes(1);
expect(onClickFilterString).toHaveBeenCalledTimes(1);
});
describe('Interacting with log details', () => {
it('Allows text selection even if the popover menu is not available', async () => {
setup({
onClickFilterOutString: undefined,
onClickFilterString: undefined,
});
await userEvent.click(screen.getByText('log message 1'));
expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
expect(screen.queryByText(/details/)).not.toBeInTheDocument();
});
it('Displays Log Details if there is no text selection', async () => {
jest.spyOn(document, 'getSelection').mockReturnValue(null);
setup({
onClickFilterOutString: undefined,
onClickFilterString: undefined,
});
await userEvent.click(screen.getByText('log message 1'));
expect(screen.queryByText('Copy selection')).not.toBeInTheDocument();
expect(screen.getByText(/Fields/)).toBeInTheDocument();
});
});
});
describe('Text search', () => {
test('Supports text search', async () => {
render(<LogList {...defaultProps} />);
expect(screen.queryByPlaceholderText('Search in logs')).not.toBeInTheDocument();
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('log message 2')).toBeInTheDocument();
await userEvent.keyboard('{Control>}{f}{/Control}');
expect(screen.getByPlaceholderText('Search in logs')).toBeInTheDocument();
await userEvent.type(screen.getByPlaceholderText('Search in logs'), 'message 2');
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.queryByText('log message 2')).not.toBeInTheDocument();
expect(screen.getByText('message 2')).toBeInTheDocument();
await userEvent.click(screen.getByLabelText('Filter matching logs'));
expect(screen.queryByText('log message 1')).not.toBeInTheDocument();
expect(screen.getByText('message 2')).toBeInTheDocument();
await userEvent.keyboard('{Escape}');
expect(screen.queryByPlaceholderText('Search in logs')).not.toBeInTheDocument();
});
test('Does not conflict with search words', async () => {
logs = [
createLogRow({ uid: '1' }),
createLogRow({ uid: '2', entry: '(?i)some text', searchWords: ['some text'] }),
];
render(<LogList {...defaultProps} logs={logs} />);
expect(screen.queryByPlaceholderText('Search in logs')).not.toBeInTheDocument();
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
await userEvent.keyboard('{Control>}{f}{/Control}');
expect(screen.getByPlaceholderText('Search in logs')).toBeInTheDocument();
await userEvent.type(screen.getByPlaceholderText('Search in logs'), '(?i)');
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('(?i)')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
await userEvent.clear(screen.getByPlaceholderText('Search in logs'));
expect(screen.getByText('log message 1')).toBeInTheDocument();
expect(screen.getByText('some text')).toBeInTheDocument();
});
});
});