import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import {changeInputValue, openMenu, selectByLabel} from 'sentry-test/select-new';
import TagStore from 'sentry/stores/tagStore';
import ColumnEditModal from 'sentry/views/eventsV2/table/columnEditModal';
const stubEl = props =>
{props.children}
;
function mountModal({columns, onApply, customMeasurements}, initialData) {
return mountWithTheme(
void 0}
customMeasurements={customMeasurements}
/>,
initialData.routerContext
);
}
describe('EventsV2 -> ColumnEditModal', function () {
enforceActOnUseLegacyStoreHook();
beforeEach(() => {
TagStore.reset();
TagStore.loadTagsSuccess([
{name: 'browser.name', key: 'browser.name', count: 1},
{name: 'custom-field', key: 'custom-field', count: 1},
{name: 'user', key: 'user', count: 1},
]);
});
const initialData = initializeOrg({
organization: {
features: ['performance-view', 'dashboards-mep'],
},
});
const columns = [
{
kind: 'field',
field: 'event.type',
},
{
kind: 'field',
field: 'browser.name',
},
{
kind: 'function',
function: ['count', 'id'],
},
{
kind: 'function',
function: ['count_unique', 'title'],
},
{
kind: 'function',
function: ['p95', ''],
},
{
kind: 'field',
field: 'issue.id',
},
{
kind: 'function',
function: ['count_unique', 'issue.id'],
},
];
describe('basic rendering', function () {
const wrapper = mountModal(
{
columns,
onApply: () => void 0,
},
initialData
);
it('renders fields and basic controls', function () {
// Should have fields equal to the columns.
expect(wrapper.find('QueryField')).toHaveLength(columns.length);
expect(wrapper.find('button[aria-label="Apply"]')).toHaveLength(1);
expect(wrapper.find('button[aria-label="Add a Column"]')).toHaveLength(1);
});
it('renders delete and grab buttons', function () {
expect(
wrapper.find('RowContainer button[aria-label="Remove column"]').length
).toEqual(columns.length);
expect(
wrapper.find('RowContainer button[aria-label="Drag to reorder"]').length
).toEqual(columns.length);
});
});
describe('rendering unknown fields', function () {
const wrapper = mountModal(
{
columns: [
{kind: 'function', function: ['count_unique', 'user-defined']},
{kind: 'field', field: 'user-def'},
],
onApply: () => void 0,
},
initialData
);
it('renders unknown fields in field and field parameter controls', function () {
const funcRow = wrapper.find('QueryField').first();
expect(
funcRow.find('SelectControl[name="field"] [data-test-id="label"]').text()
).toBe('count_unique(\u2026)');
expect(
funcRow
.find('SelectControl[name="parameter"] SingleValue span[data-test-id="label"]')
.text()
).toBe('user-defined');
const fieldRow = wrapper.find('QueryField').last();
expect(
fieldRow.find('SelectControl[name="field"] span[data-test-id="label"]').text()
).toBe('user-def');
expect(fieldRow.find('SelectControl[name="field"] Tag')).toHaveLength(1);
expect(fieldRow.find('BlankSpace')).toHaveLength(1);
});
});
describe('rendering tags that overlap fields & functions', function () {
const wrapper = mountModal(
{
columns: [
{kind: 'field', field: 'tags[project]'},
{kind: 'field', field: 'tags[count]'},
],
onApply: () => void 0,
},
initialData
);
beforeEach(() => {
TagStore.reset();
TagStore.loadTagsSuccess([
{name: 'project', key: 'project', count: 1},
{name: 'count', key: 'count', count: 1},
]);
});
it('selects tag expressions that overlap fields', function () {
const funcRow = wrapper.find('QueryField').first();
expect(
funcRow.find('SelectControl[name="field"] span[data-test-id="label"]').text()
).toBe('project');
expect(funcRow.find('SelectControl[name="field"] Tag')).toHaveLength(1);
});
it('selects tag expressions that overlap functions', function () {
const funcRow = wrapper.find('QueryField').last();
expect(
funcRow.find('SelectControl[name="field"] span[data-test-id="label"]').text()
).toBe('count');
expect(funcRow.find('SelectControl[name="field"] Tag')).toHaveLength(1);
});
});
describe('rendering functions', function () {
const wrapper = mountModal(
{
columns: [
{kind: 'function', function: ['count', 'id']},
{kind: 'function', function: ['count_unique', 'title']},
{kind: 'function', function: ['percentile', 'transaction.duration', '0.66']},
],
onApply: () => void 0,
},
initialData
);
it('renders three columns when needed', function () {
const countRow = wrapper.find('QueryField').first();
// Has a select and 2 disabled inputs
expect(countRow.find('SelectControl')).toHaveLength(1);
expect(countRow.find('BlankSpace')).toHaveLength(2);
const percentileRow = wrapper.find('QueryField').last();
// two select fields, and one number input.
expect(percentileRow.find('SelectControl')).toHaveLength(2);
expect(percentileRow.find('BlankSpace')).toHaveLength(0);
expect(percentileRow.find('StyledInput[inputMode="numeric"]')).toHaveLength(1);
});
});
describe('function & column selection', function () {
let onApply, wrapper;
beforeEach(function () {
onApply = jest.fn();
wrapper = mountModal(
{
columns: [columns[0]],
onApply,
},
initialData
);
});
it('restricts column choices', function () {
selectByLabel(wrapper, 'avg(\u2026)', {name: 'field', at: 0, control: true});
openMenu(wrapper, {name: 'parameter', at: 0, control: true});
const options = wrapper
.find('QueryField SelectControl[name="parameter"] Option')
.map(option => option.props().label);
expect(options).not.toContain('title');
expect(options).toContain('transaction.duration');
});
it('shows no options for parameterless functions', function () {
selectByLabel(wrapper, 'last_seen()', {name: 'field', at: 0, control: true});
expect(wrapper.find('QueryField BlankSpace')).toHaveLength(1);
});
it('shows additional inputs for multi-parameter functions', function () {
selectByLabel(wrapper, 'percentile(\u2026)', {name: 'field', at: 0, control: true});
// Parameter select should display and use the default value.
const field = wrapper.find('QueryField SelectControl[name="parameter"]');
expect(field.find('SingleValue span[data-test-id="label"]').text()).toBe(
'transaction.duration'
);
// Input should show and have default value.
const refinement = wrapper.find('QueryField input[inputMode="numeric"]');
expect(refinement.props().value).toBe('0.5');
});
it('handles scalar field parameters', function () {
selectByLabel(wrapper, 'apdex(\u2026)', {name: 'field', at: 0, control: true});
// Parameter select should display and use the default value.
const field = wrapper.find('QueryField input[name="refinement"]');
expect(field.props().value).toBe('300');
// Trigger a blur and make sure the column is not wrong.
field.simulate('blur');
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['apdex', '300', undefined, undefined]},
]);
});
it('handles parameter overrides', function () {
selectByLabel(wrapper, 'apdex(\u2026)', {name: 'field', at: 0, control: true});
// Parameter select should display and use the default value.
const field = wrapper.find('QueryField input[name="refinement"]');
expect(field.props().value).toBe('300');
expect(field.prop('placeholder')).toBe(undefined);
// Trigger a blur and make sure the column is not wrong.
field.simulate('blur');
});
it('clears unused parameters', function () {
// Choose percentile, then apdex which has fewer parameters and different types.
selectByLabel(wrapper, 'percentile(\u2026)', {name: 'field', at: 0, control: true});
selectByLabel(wrapper, 'apdex(\u2026)', {name: 'field', at: 0, control: true});
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['apdex', '300', undefined, undefined]},
]);
});
it('clears all unused parameters', function () {
// Choose percentile, then failure_rate which has no parameters.
selectByLabel(wrapper, 'percentile(\u2026)', {name: 'field', at: 0, control: true});
selectByLabel(wrapper, 'failure_rate()', {name: 'field', at: 0, control: true});
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['failure_rate', '', undefined, undefined]},
]);
});
it('clears all unused parameters with count_if to two parameter function', function () {
// Choose percentile, then failure_rate which has no parameters.
selectByLabel(wrapper, 'count_if(\u2026)', {name: 'field', at: 0, control: true});
selectByLabel(wrapper, 'user', {name: 'parameter', at: 0, control: true});
selectByLabel(wrapper, 'count_miserable(\u2026)', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count_miserable', 'user', '300', undefined]},
]);
});
it('clears all unused parameters with count_if to one parameter function', function () {
// Choose percentile, then failure_rate which has no parameters.
selectByLabel(wrapper, 'count_if(\u2026)', {name: 'field', at: 0, control: true});
selectByLabel(wrapper, 'user', {name: 'parameter', at: 0, control: true});
selectByLabel(wrapper, 'count_unique(\u2026)', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count_unique', 'user', undefined, undefined]},
]);
});
it('clears all unused parameters with count_if to parameterless function', function () {
// Choose percentile, then failure_rate which has no parameters.
selectByLabel(wrapper, 'count_if(\u2026)', {name: 'field', at: 0, control: true});
selectByLabel(wrapper, 'count()', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
wrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count', '', undefined, undefined]},
]);
});
it('updates equation errors when they change', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'equation',
field: '1 / 0',
},
],
onApply,
},
initialData
);
expect(newWrapper.find('QueryField ArithmeticError')).toHaveLength(1);
expect(newWrapper.find('QueryField ArithmeticError').prop('title')).toBe(
'Division by 0 is not allowed'
);
const field = newWrapper.find('QueryField input[type="text"]');
changeInputValue(field, '1+1+1+1+1+1+1+1+1+1+1+1');
newWrapper.update();
field.simulate('blur');
expect(newWrapper.find('QueryField ArithmeticError').prop('title')).toBe(
'Maximum operators exceeded'
);
});
it('resets required field to previous value if cleared', function () {
const initialColumnVal = '0.6';
const newWrapper = mountModal(
{
columns: [
{
kind: 'function',
function: [
'percentile',
'transaction.duration',
initialColumnVal,
undefined,
],
},
],
onApply,
},
initialData
);
const field = newWrapper.find('QueryField input[name="refinement"]');
changeInputValue(field, '');
newWrapper.update();
field.simulate('blur');
expect(newWrapper.find('QueryField input[name="refinement"]').prop('value')).toBe(
initialColumnVal
);
newWrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{
kind: 'function',
function: ['percentile', 'transaction.duration', initialColumnVal, undefined],
},
]);
});
});
describe('equation automatic update', function () {
let onApply;
beforeEach(function () {
onApply = jest.fn();
});
it('update simple equation columns when they change', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'function',
function: ['count_unique', 'user'],
},
{
kind: 'function',
function: ['p95', ''],
},
{
kind: 'equation',
field: '(p95() / count_unique(user) ) * 100',
},
],
onApply,
},
initialData
);
selectByLabel(newWrapper, 'count_if(\u2026)', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
newWrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count_if', 'user', 'equals', '300']},
{kind: 'function', function: ['p95', '']},
{kind: 'equation', field: '(p95() / count_if(user,equals,300) ) * 100'},
]);
});
it('update equation with repeated columns when they change', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'function',
function: ['count_unique', 'user'],
},
{
kind: 'equation',
field:
'count_unique(user) + (count_unique(user) - count_unique(user)) * 5',
},
],
onApply,
},
initialData
);
selectByLabel(newWrapper, 'count()', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
newWrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count', '', undefined, undefined]},
{kind: 'equation', field: 'count() + (count() - count()) * 5'},
]);
});
it('handles equations with duplicate fields', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'field',
field: 'spans.db',
},
{
kind: 'field',
field: 'spans.db',
},
{
kind: 'equation',
field: 'spans.db - spans.db',
},
],
onApply,
},
initialData
);
selectByLabel(newWrapper, 'count()', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
newWrapper.find('Button[priority="primary"]').simulate('click');
// Because spans.db is still a selected column it isn't swapped
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count', '', undefined, undefined]},
{kind: 'field', field: 'spans.db'},
{kind: 'equation', field: 'spans.db - spans.db'},
]);
});
it('handles equations with duplicate functions', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'function',
function: ['count', '', undefined, undefined],
},
{
kind: 'function',
function: ['count', '', undefined, undefined],
},
{
kind: 'equation',
field: 'count() - count()',
},
],
onApply,
},
initialData
);
selectByLabel(newWrapper, 'count_unique(\u2026)', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
newWrapper.find('Button[priority="primary"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count_unique', 'user', undefined, undefined]},
{kind: 'function', function: ['count', '', undefined, undefined]},
{kind: 'equation', field: 'count() - count()'},
]);
});
it('handles incomplete equations', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'function',
function: ['count', '', undefined, undefined],
},
{
kind: 'equation',
field: 'count() - count() arst count() ',
},
],
onApply,
},
initialData
);
expect(newWrapper.find('QueryField ArithmeticError')).toHaveLength(1);
selectByLabel(newWrapper, 'count_unique(\u2026)', {
name: 'field',
at: 0,
control: true,
});
// Apply the changes so we can see the new columns.
newWrapper.find('Button[priority="primary"]').simulate('click');
// With the way the parser works only tokens up to the error will be updated
expect(onApply).toHaveBeenCalledWith([
{kind: 'function', function: ['count_unique', 'user', undefined, undefined]},
{
kind: 'equation',
field: 'count_unique(user) - count_unique(user) arst count() ',
},
]);
});
});
describe('adding rows', function () {
const wrapper = mountModal(
{
columns: [columns[0]],
onApply: () => void 0,
},
initialData
);
it('allows rows to be added, but only up to 20', function () {
for (let i = 2; i <= 20; i++) {
wrapper.find('button[aria-label="Add a Column"]').simulate('click');
expect(wrapper.find('QueryField')).toHaveLength(i);
}
expect(
wrapper.find('button[aria-label="Add a Column"]').prop('aria-disabled')
).toBe(true);
});
});
describe('removing rows', function () {
const wrapper = mountModal(
{
columns: [columns[0], columns[1]],
onApply: () => void 0,
},
initialData
);
it('allows rows to be removed, but not the last one', function () {
expect(wrapper.find('QueryField')).toHaveLength(2);
wrapper
.find('RowContainer button[aria-label="Remove column"]')
.first()
.simulate('click');
expect(wrapper.find('QueryField')).toHaveLength(1);
// Last row cannot be removed or dragged.
expect(
wrapper.find('RowContainer button[aria-label="Remove column"]')
).toHaveLength(0);
expect(
wrapper.find('RowContainer button[aria-label="Drag to reorder"]')
).toHaveLength(0);
});
it('does not count equations towards the count of rows', function () {
const newWrapper = mountModal(
{
columns: [
columns[0],
columns[1],
{
kind: 'equation',
field: '5 + 5',
},
],
onApply: () => void 0,
},
initialData
);
expect(newWrapper.find('QueryField')).toHaveLength(3);
newWrapper
.find('RowContainer button[aria-label="Remove column"]')
.first()
.simulate('click');
expect(newWrapper.find('QueryField')).toHaveLength(2);
// Can still remove the equation
expect(
newWrapper.find('RowContainer button[aria-label="Remove column"]')
).toHaveLength(1);
// And both are draggable
expect(
newWrapper.find('RowContainer button[aria-label="Drag to reorder"]')
).toHaveLength(2);
});
it('handles equations being deleted', function () {
const newWrapper = mountModal(
{
columns: [
{
kind: 'equation',
field: '1 / 0',
},
columns[0],
columns[1],
],
onApply: () => void 0,
},
initialData
);
expect(newWrapper.find('QueryField ArithmeticError')).toHaveLength(1);
expect(newWrapper.find('QueryField')).toHaveLength(3);
newWrapper
.find('RowContainer button[aria-label="Remove column"]')
.first()
.simulate('click');
expect(newWrapper.find('QueryField')).toHaveLength(2);
expect(newWrapper.find('ArithmeticError')).toHaveLength(0);
});
});
describe('apply action', function () {
const onApply = jest.fn();
const wrapper = mountModal(
{
columns: [columns[0], columns[1]],
onApply,
},
initialData
);
it('reflects added and removed columns', function () {
// Remove a column, then add a blank one an select a value in it.
wrapper.find('button[aria-label="Remove column"]').first().simulate('click');
wrapper.find('button[aria-label="Add a Column"]').simulate('click');
wrapper.update();
selectByLabel(wrapper, 'title', {name: 'field', at: 1, control: true});
wrapper.find('button[aria-label="Apply"]').simulate('click');
expect(onApply).toHaveBeenCalledWith([columns[1], {kind: 'field', field: 'title'}]);
});
});
describe('custom performance metrics', function () {
it('allows selecting custom performance metrics in dropdown', function () {
render(
undefined}
closeModal={() => undefined}
customMeasurements={{
'measurements.custom.kibibyte': {
key: 'measurements.custom.kibibyte',
name: 'measurements.custom.kibibyte',
functions: ['p99'],
},
'measurements.custom.minute': {
key: 'measurements.custom.minute',
name: 'measurements.custom.minute',
functions: ['p99'],
},
'measurements.custom.ratio': {
key: 'measurements.custom.ratio',
name: 'measurements.custom.ratio',
functions: ['p99'],
},
}}
/>
);
expect(screen.getByText('event.type')).toBeInTheDocument();
userEvent.click(screen.getByText('event.type'));
userEvent.type(screen.getAllByText('event.type')[0], 'custom');
expect(screen.getByText('measurements.custom.kibibyte')).toBeInTheDocument();
});
});
});