import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { createTheme } from '@grafana/data';
import LokiLanguageProvider from '../language_provider';
import {
buildSelector,
facetLabels,
SelectableLabel,
UnthemedLokiLabelBrowser,
BrowserProps,
} from './LokiLabelBrowser';
// we have to mock out reportInteraction, otherwise it crashes the test.
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
reportInteraction: () => null,
}));
describe('buildSelector()', () => {
it('returns an empty selector for no labels', () => {
expect(buildSelector([])).toEqual('{}');
});
it('returns an empty selector for selected labels with no values', () => {
const labels: SelectableLabel[] = [{ name: 'foo', selected: true }];
expect(buildSelector(labels)).toEqual('{}');
});
it('returns an empty selector for one selected label with no selected values', () => {
const labels: SelectableLabel[] = [{ name: 'foo', selected: true, values: [{ name: 'bar' }] }];
expect(buildSelector(labels)).toEqual('{}');
});
it('returns a simple selector from a selected label with a selected value', () => {
const labels: SelectableLabel[] = [{ name: 'foo', selected: true, values: [{ name: 'bar', selected: true }] }];
expect(buildSelector(labels)).toEqual('{foo="bar"}');
});
});
describe('facetLabels()', () => {
const possibleLabels = {
cluster: ['dev'],
namespace: ['alertmanager'],
};
const labels: SelectableLabel[] = [
{ name: 'foo', selected: true, values: [{ name: 'bar' }] },
{ name: 'cluster', values: [{ name: 'dev' }, { name: 'ops' }, { name: 'prod' }] },
{ name: 'namespace', values: [{ name: 'alertmanager' }] },
];
it('returns no labels given an empty label set', () => {
expect(facetLabels([], {})).toEqual([]);
});
it('marks all labels as hidden when no labels are possible', () => {
const result = facetLabels(labels, {});
expect(result.length).toEqual(labels.length);
expect(result[0].hidden).toBeTruthy();
expect(result[0].values).toBeUndefined();
});
it('keeps values as facetted when they are possible', () => {
const result = facetLabels(labels, possibleLabels);
expect(result.length).toEqual(labels.length);
expect(result[0].hidden).toBeTruthy();
expect(result[0].values).toBeUndefined();
expect(result[1].hidden).toBeFalsy();
expect(result[1].values!.length).toBe(1);
expect(result[1].values![0].name).toBe('dev');
});
it('does not facet out label values that are currently being facetted', () => {
const result = facetLabels(labels, possibleLabels, 'cluster');
expect(result.length).toEqual(labels.length);
expect(result[0].hidden).toBeTruthy();
expect(result[1].hidden).toBeFalsy();
// 'cluster' is being facetted, should show all 3 options even though only 1 is possible
expect(result[1].values!.length).toBe(3);
expect(result[2].values!.length).toBe(1);
});
});
describe('LokiLabelBrowser', () => {
const setupProps = (): BrowserProps => {
const mockLanguageProvider = {
start: () => Promise.resolve(),
getLabelValues: (name: string) => {
switch (name) {
case 'label1':
return ['value1-1', 'value1-2'];
case 'label2':
return ['value2-1', 'value2-2'];
case 'label3':
return ['value3-1', 'value3-2'];
}
return [];
},
fetchSeriesLabels: (selector: string) => {
switch (selector) {
case '{label1="value1-1"}':
return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] };
case '{label1=~"value1-1|value1-2"}':
return { label1: ['value1-1', 'value1-2'], label2: ['value2-1'], label3: ['value3-1', 'value3-2'] };
}
// Allow full set by default
return {
label1: ['value1-1', 'value1-2'],
label2: ['value2-1', 'value2-2'],
};
},
getLabelKeys: () => ['label1', 'label2', 'label3'],
};
const defaults: BrowserProps = {
theme: createTheme(),
onChange: () => {},
autoSelect: 0,
languageProvider: mockLanguageProvider as unknown as LokiLanguageProvider,
lastUsedLabels: [],
storeLastUsedLabels: () => {},
deleteLastUsedLabels: () => {},
};
return defaults;
};
// Clear label selection manually because it's saved in localStorage
afterEach(async () => {
const clearBtn = screen.getByLabelText('Selector clear button');
await userEvent.click(clearBtn);
});
it('renders and loader shows when empty, and then first set of labels', async () => {
const props = setupProps();
render();
// Loading appears and dissappears
screen.getByText(/Loading labels/);
await waitFor(() => {
expect(screen.queryByText(/Loading labels/)).not.toBeInTheDocument();
});
// Initial set of labels is available and not selected
expect(screen.queryByRole('option', { name: 'label1' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'label1', selected: true })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'label2' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'label2', selected: true })).not.toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
});
it('allows label and value selection/deselection', async () => {
const props = setupProps();
render();
// Selecting label2
const label2 = await screen.findByRole('option', { name: 'label2', selected: false });
expect(screen.queryByRole('list', { name: /Values/ })).not.toBeInTheDocument();
await userEvent.click(label2);
expect(screen.queryByRole('option', { name: 'label2', selected: true })).toBeInTheDocument();
// List of values for label2 appears
expect(await screen.findAllByRole('list')).toHaveLength(1);
expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
// Selecting label1, list for its values appears
const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
await userEvent.click(label1);
expect(screen.queryByRole('option', { name: 'label1', selected: true })).toBeInTheDocument();
await screen.findByLabelText('Values for label1');
expect(await screen.findAllByRole('list')).toHaveLength(2);
// Selecting value2-2 of label2
const value = await screen.findByRole('option', { name: 'value2-2', selected: false });
await userEvent.click(value);
await screen.findByRole('option', { name: 'value2-2', selected: true });
expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-2"}');
// Selecting value2-1 of label2, both values now selected
const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
await userEvent.click(value2);
// await screen.findByRole('option', {name: 'value2-1', selected: true});
await screen.findByText('{label2=~"value2-1|value2-2"}');
// Deselecting value2-2, one value should remain
const selectedValue = await screen.findByRole('option', { name: 'value2-2', selected: true });
await userEvent.click(selectedValue);
await screen.findByRole('option', { name: 'value2-1', selected: true });
await screen.findByRole('option', { name: 'value2-2', selected: false });
expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-1"}');
});
it('allows label selection from multiple labels', async () => {
const props = setupProps();
render();
// Selecting label2
const label2 = await screen.findByRole('option', { name: /label2/, selected: false });
await userEvent.click(label2);
// List of values for label2 appears
expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
// Selecting label1, list for its values appears
const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
await userEvent.click(label1);
await screen.findByLabelText('Values for label1');
expect(await screen.findAllByRole('list')).toHaveLength(2);
// Selecting value2-1 of label2
const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
await userEvent.click(value2);
await screen.findByText('{label2="value2-1"}');
// Selecting value from label1 for combined selector
const value1 = await screen.findByRole('option', { name: 'value1-2', selected: false });
await userEvent.click(value1);
await screen.findByRole('option', { name: 'value1-2', selected: true });
await screen.findByText('{label1="value1-2",label2="value2-1"}');
// Deselect label1 should remove label and value
const selectedLabel = (await screen.findAllByRole('option', { name: /label1/, selected: true }))[0];
await userEvent.click(selectedLabel);
await screen.findByRole('option', { name: /label1/, selected: false });
expect(await screen.findAllByRole('list')).toHaveLength(1);
expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-1"}');
});
it('allows clearing the label selection', async () => {
const props = setupProps();
render();
// Selecting label2
const label2 = await screen.findByRole('option', { name: 'label2', selected: false });
await userEvent.click(label2);
// List of values for label2 appears
expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
// Selecting label1, list for its values appears
const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
await userEvent.click(label1);
await screen.findByLabelText('Values for label1');
expect(await screen.findAllByRole('list')).toHaveLength(2);
// Selecting value2-1 of label2
const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
await userEvent.click(value2);
await screen.findByText('{label2="value2-1"}');
// Clear selector
const clearBtn = screen.getByLabelText('Selector clear button');
await userEvent.click(clearBtn);
await screen.findByRole('option', { name: 'label2', selected: false });
expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
});
it('filters values by input text', async () => {
const props = setupProps();
render();
// Selecting label2 and label1
const label2 = await screen.findByRole('option', { name: /label2/, selected: false });
await userEvent.click(label2);
const label1 = await screen.findByRole('option', { name: /label1/, selected: false });
await userEvent.click(label1);
await screen.findByLabelText('Values for label1');
await screen.findByLabelText('Values for label2');
expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(4);
// Typing '1' to filter for values
await userEvent.type(screen.getByLabelText('Filter expression for values'), 'val1');
expect(screen.getByLabelText('Filter expression for values')).toHaveValue('val1');
expect(screen.queryByRole('option', { name: 'value2-2' })).not.toBeInTheDocument();
expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(3);
});
it('facets labels', async () => {
const props = setupProps();
render();
// Selecting label2 and label1
const label2 = await screen.findByRole('option', { name: /label2/, selected: false });
await userEvent.click(label2);
const label1 = await screen.findByRole('option', { name: /label1/, selected: false });
await userEvent.click(label1);
await screen.findByLabelText('Values for label1');
await screen.findByLabelText('Values for label2');
expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(4);
expect(screen.queryByRole('option', { name: /label3/ })).toHaveTextContent('label3');
// Click value1-1 which triggers facetting for value3-x, and still show all value1-x
const value1 = await screen.findByRole('option', { name: 'value1-1', selected: false });
await userEvent.click(value1);
await waitFor(() => expect(screen.queryByRole('option', { name: 'value2-2' })).not.toBeInTheDocument());
expect(screen.queryByRole('option', { name: 'value1-2' })).toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{label1="value1-1"}');
expect(screen.queryByRole('option', { name: /label3/ })).toHaveTextContent('label3 (1)');
// Click value1-2 for which facetting will allow more values for value3-x
const value12 = await screen.findByRole('option', { name: 'value1-2', selected: false });
await userEvent.click(value12);
await screen.findByRole('option', { name: 'value1-2', selected: true });
await userEvent.click(screen.getByRole('option', { name: /label3/ }));
await screen.findByLabelText('Values for label3');
expect(screen.queryByRole('option', { name: 'value1-1', selected: true })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'value1-2', selected: true })).toBeInTheDocument();
expect(screen.queryByLabelText('selector')).toHaveTextContent('{label1=~"value1-1|value1-2"}');
expect(screen.queryAllByRole('option', { name: /label3/ })[0]).toHaveTextContent('label3 (2)');
});
});