import {mountWithTheme} from 'sentry-test/enzyme'; import EventView from 'sentry/utils/discover/eventView'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import CellAction, {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction'; const defaultData = { transaction: 'best-transaction', count: 19, timestamp: '2020-06-09T01:46:25+00:00', release: 'F2520C43515BD1F0E8A6BD46233324641A370BF6', nullValue: null, 'measurements.fcp': 1234, 'percentile(measurements.fcp, 0.5)': 1234, 'error.handled': [null], 'error.type': [ 'ServerException', 'ClickhouseError', 'QueryException', 'QueryException', ], }; function makeWrapper(eventView, handleCellAction, columnIndex = 0, data = defaultData) { return mountWithTheme( some content ); } describe('Discover -> CellAction', function () { const location = { query: { id: '42', name: 'best query', field: [ 'transaction', 'count()', 'timestamp', 'release', 'nullValue', 'measurements.fcp', 'percentile(measurements.fcp, 0.5)', 'error.handled', 'error.type', ], widths: ['437', '647', '416', '905'], sort: ['title'], query: 'event.type:transaction', project: [123], start: '2019-10-01T00:00:00', end: '2019-10-02T00:00:00', statsPeriod: '14d', environment: ['staging'], yAxis: 'p95', }, }; const view = EventView.fromLocation(location); describe('hover menu button', function () { const wrapper = makeWrapper(view); it('shows no menu by default', function () { expect(wrapper.find('MenuButton')).toHaveLength(0); }); it('shows a menu on hover, and hides again', function () { wrapper.find('Container').simulate('mouseEnter'); expect(wrapper.find('MenuButton')).toHaveLength(1); wrapper.find('Container').simulate('mouseLeave'); expect(wrapper.find('MenuButton')).toHaveLength(0); }); }); describe('opening the menu', function () { const wrapper = makeWrapper(view); wrapper.find('Container').simulate('mouseEnter'); it('toggles the menu on click', function () { // Button should be rendered. expect(wrapper.find('MenuButton')).toHaveLength(1); wrapper.find('MenuButton').simulate('click'); // Menu should show now. expect(wrapper.find('Menu')).toHaveLength(1); }); }); describe('per cell actions', function () { let wrapper; let handleCellAction; beforeEach(function () { handleCellAction = jest.fn(); wrapper = makeWrapper(view, handleCellAction); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); }); it('add button appends condition', function () { wrapper.find('button[data-test-id="add-to-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('add', 'best-transaction'); }); it('exclude button adds condition', function () { wrapper.find('button[data-test-id="exclude-from-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction'); }); it('exclude button appends exclusions', function () { const excludeView = EventView.fromLocation({ query: {...location.query, query: '!transaction:nope'}, }); wrapper = makeWrapper(excludeView, handleCellAction); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="exclude-from-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('exclude', 'best-transaction'); }); it('go to summary button goes to transaction summary page', function () { wrapper.find('button[data-test-id="transaction-summary"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('transaction', 'best-transaction'); }); it('go to release button goes to release health page', function () { wrapper = makeWrapper(view, handleCellAction, 3); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="release"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith( 'release', 'F2520C43515BD1F0E8A6BD46233324641A370BF6' ); }); it('greater than button adds condition', function () { wrapper = makeWrapper(view, handleCellAction, 2); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="show-values-greater-than"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith( 'show_greater_than', '2020-06-09T01:46:25+00:00' ); }); it('less than button adds condition', function () { wrapper = makeWrapper(view, handleCellAction, 2); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="show-values-less-than"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith( 'show_less_than', '2020-06-09T01:46:25+00:00' ); }); it('error.handled with null adds condition', function () { wrapper = makeWrapper(view, handleCellAction, 7, defaultData); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="add-to-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('add', 1); }); it('error.type with array values adds condition', function () { wrapper = makeWrapper(view, handleCellAction, 8, defaultData); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="add-to-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('add', [ 'ServerException', 'ClickhouseError', 'QueryException', 'QueryException', ]); }); it('error.handled with 0 adds condition', function () { wrapper = makeWrapper(view, handleCellAction, 7, { ...defaultData, 'error.handled': [0], }); // Show button and menu. wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); wrapper.find('button[data-test-id="add-to-filter"]').simulate('click'); expect(handleCellAction).toHaveBeenCalledWith('add', [0]); }); it('show appropriate actions for string cells', function () { wrapper = makeWrapper(view, handleCellAction, 0); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeTruthy(); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeFalsy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeFalsy(); }); it('show appropriate actions for string cells with null values', function () { wrapper = makeWrapper(view, handleCellAction, 4); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeTruthy(); }); it('show appropriate actions for number cells', function () { wrapper = makeWrapper(view, handleCellAction, 1); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeFalsy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeFalsy(); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeTruthy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeTruthy(); }); it('show appropriate actions for date cells', function () { wrapper = makeWrapper(view, handleCellAction, 2); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeFalsy(); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeTruthy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeTruthy(); }); it('show appropriate actions for release cells', function () { wrapper = makeWrapper(view, handleCellAction, 3); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="release"]').exists()).toBeTruthy(); wrapper = makeWrapper(view, handleCellAction, 3, { ...defaultData, release: null, }); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect(wrapper.find('button[data-test-id="release"]').exists()).toBeFalsy(); }); it('show appropriate actions for measurement cells', function () { wrapper = makeWrapper(view, handleCellAction, 5); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeTruthy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeTruthy(); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeFalsy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeFalsy(); wrapper = makeWrapper(view, handleCellAction, 5, { ...defaultData, 'measurements.fcp': null, }); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeFalsy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeFalsy(); expect(wrapper.find('button[data-test-id="add-to-filter"]').exists()).toBeTruthy(); expect( wrapper.find('button[data-test-id="exclude-from-filter"]').exists() ).toBeTruthy(); }); it('show appropriate actions for numeric function cells', function () { wrapper = makeWrapper(view, handleCellAction, 6); wrapper.find('Container').simulate('mouseEnter'); wrapper.find('MenuButton').simulate('click'); expect( wrapper.find('button[data-test-id="show-values-greater-than"]').exists() ).toBeTruthy(); expect( wrapper.find('button[data-test-id="show-values-less-than"]').exists() ).toBeTruthy(); wrapper = makeWrapper(view, handleCellAction, 6, { ...defaultData, 'percentile(measurements.fcp, 0.5)': null, }); wrapper.find('Container').simulate('mouseEnter'); expect(wrapper.find('MenuButton').exists()).toBeFalsy(); }); }); }); describe('updateQuery()', function () { const columnA = { key: 'a', name: 'a', type: 'number', isSortable: false, column: { kind: 'field', field: 'a', }, width: -1, }; const columnB = { key: 'b', name: 'b', type: 'number', isSortable: false, column: { kind: 'field', field: 'b', }, width: -1, }; it('modifies the query with has/!has', function () { let results = new MutableSearch([]); updateQuery(results, Actions.ADD, columnA, null); expect(results.formatString()).toEqual('!has:a'); updateQuery(results, Actions.EXCLUDE, columnA, null); expect(results.formatString()).toEqual('has:a'); updateQuery(results, Actions.ADD, columnA, null); expect(results.formatString()).toEqual('!has:a'); results = new MutableSearch([]); updateQuery(results, Actions.ADD, columnA, [null]); expect(results.formatString()).toEqual('!has:a'); }); it('modifies the query with additions', function () { const results = new MutableSearch([]); updateQuery(results, Actions.ADD, columnA, '1'); expect(results.formatString()).toEqual('a:1'); updateQuery(results, Actions.ADD, columnB, '1'); expect(results.formatString()).toEqual('a:1 b:1'); updateQuery(results, Actions.ADD, columnA, '2'); expect(results.formatString()).toEqual('b:1 a:2'); updateQuery(results, Actions.ADD, columnA, ['1', '2', '3']); expect(results.formatString()).toEqual('b:1 a:2 a:1 a:3'); }); it('modifies the query with exclusions', function () { const results = new MutableSearch([]); updateQuery(results, Actions.EXCLUDE, columnA, '1'); expect(results.formatString()).toEqual('!a:1'); updateQuery(results, Actions.EXCLUDE, columnB, '1'); expect(results.formatString()).toEqual('!a:1 !b:1'); updateQuery(results, Actions.EXCLUDE, columnA, '2'); expect(results.formatString()).toEqual('!b:1 !a:1 !a:2'); updateQuery(results, Actions.EXCLUDE, columnA, ['1', '2', '3']); expect(results.formatString()).toEqual('!b:1 !a:1 !a:2 !a:3'); }); it('modifies the query with a mix of additions and exclusions', function () { const results = new MutableSearch([]); updateQuery(results, Actions.ADD, columnA, '1'); expect(results.formatString()).toEqual('a:1'); updateQuery(results, Actions.ADD, columnB, '2'); expect(results.formatString()).toEqual('a:1 b:2'); updateQuery(results, Actions.EXCLUDE, columnA, '3'); expect(results.formatString()).toEqual('b:2 !a:3'); updateQuery(results, Actions.EXCLUDE, columnB, '4'); expect(results.formatString()).toEqual('!a:3 !b:4'); updateQuery(results, Actions.ADD, columnA, '5'); expect(results.formatString()).toEqual('!b:4 a:5'); updateQuery(results, Actions.ADD, columnB, '6'); expect(results.formatString()).toEqual('a:5 b:6'); }); it('modifies the query with greater/less than', function () { const results = new MutableSearch([]); updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 1); expect(results.formatString()).toEqual('a:>1'); updateQuery(results, Actions.SHOW_GREATER_THAN, columnA, 2); expect(results.formatString()).toEqual('a:>2'); updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 3); expect(results.formatString()).toEqual('a:<3'); updateQuery(results, Actions.SHOW_LESS_THAN, columnA, 4); expect(results.formatString()).toEqual('a:<4'); }); it('modifies the query with greater/less than on duration fields', function () { const columnADuration = {...columnA, type: 'duration'}; const results = new MutableSearch([]); updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 1); expect(results.formatString()).toEqual('a:>1.00ms'); updateQuery(results, Actions.SHOW_GREATER_THAN, columnADuration, 2); expect(results.formatString()).toEqual('a:>2.00ms'); updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 3); expect(results.formatString()).toEqual('a:<3.00ms'); updateQuery(results, Actions.SHOW_LESS_THAN, columnADuration, 4.1234); expect(results.formatString()).toEqual('a:<4.12ms'); }); it('does not error for special actions', function () { const results = new MutableSearch([]); updateQuery(results, Actions.TRANSACTION, columnA, ''); updateQuery(results, Actions.RELEASE, columnA, ''); updateQuery(results, Actions.DRILLDOWN, columnA, ''); }); it('errors for unknown actions', function () { const results = new MutableSearch([]); expect(() => updateQuery(results, 'unknown', columnA, '')).toThrow(); }); });