import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import type {NewQuery} from 'sentry/types'; import EventView from 'sentry/utils/discover/eventView'; import {DisplayModes} from 'sentry/utils/discover/types'; import {ALL_VIEWS} from 'sentry/views/discover/data'; import SavedQueryButtonGroup from 'sentry/views/discover/savedQuery'; import * as utils from 'sentry/views/discover/savedQuery/utils'; jest.mock('sentry/actionCreators/modal'); function mount( location, organization, router, eventView, savedQuery, yAxis, disabled = false, setSavedQuery = jest.fn() ) { return render( {}} yAxis={yAxis} router={router} queryDataLoading={false} setSavedQuery={setSavedQuery} setHomepageQuery={jest.fn()} /> ); } describe('Discover > SaveQueryButtonGroup', function () { let organization; const location = { pathname: '/organization/eventsv2/', query: {}, }; const router = { location: {query: {}}, }; const yAxis = ['count()', 'failure_count()']; const errorsQuery = { ...(ALL_VIEWS.find(view => view.name === 'Errors by Title') as NewQuery), yAxis: ['count()'], display: DisplayModes.DEFAULT, }; const errorsView = EventView.fromSavedQuery(errorsQuery); const errorsViewSaved = EventView.fromSavedQuery(errorsQuery); errorsViewSaved.id = '1'; const errorsViewModified = EventView.fromSavedQuery(errorsQuery); errorsViewModified.id = '1'; errorsViewModified.name = 'Modified Name'; const savedQuery = { ...errorsViewSaved.toNewQuery(), yAxis, dateCreated: '', dateUpdated: '', id: '1', }; beforeEach(() => { organization = OrganizationFixture({ features: ['discover-query', 'dashboards-edit'], }); }); afterEach(() => { MockApiClient.clearMockResponses(); jest.clearAllMocks(); }); describe('building on a new query', () => { const mockUtils = jest .spyOn(utils, 'handleCreateQuery') .mockImplementation(() => Promise.resolve(savedQuery)); beforeEach(() => { mockUtils.mockClear(); }); it('renders disabled buttons when disabled prop is used', () => { mount(location, organization, router, errorsView, undefined, yAxis, true); expect(screen.getByRole('button', {name: /save as/i})).toBeDisabled(); }); it('renders the correct set of buttons', async () => { mount(location, organization, router, errorsView, undefined, yAxis); expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument(); expect( screen.queryByRole('button', {name: /save changes/i}) ).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.queryByRole('menuitemradio', {name: /delete saved query/i}) ).not.toBeInTheDocument(); }); it('renders the correct set of buttons with the homepage query feature', async () => { organization = OrganizationFixture({ features: ['discover-query', 'dashboards-edit'], }); mount(location, organization, router, errorsView, undefined, yAxis); expect(screen.getByRole('button', {name: /save as/i})).toBeInTheDocument(); expect(screen.getByRole('button', {name: /set as default/i})).toBeInTheDocument(); expect(screen.getByRole('button', {name: /saved queries/i})).toBeInTheDocument(); expect( screen.getByRole('button', {name: /discover context menu/i}) ).toBeInTheDocument(); expect( screen.queryByRole('button', {name: /save changes/i}) ).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.queryByRole('menuitemradio', {name: /add to dashboard/i}) ).toBeInTheDocument(); }); it('hides the banner when save is complete.', async () => { mount(location, organization, router, errorsView, undefined, yAxis); // Click on ButtonSaveAs to open dropdown await userEvent.click(screen.getByRole('button', {name: 'Save as'})); // Fill in the Input await userEvent.type( screen.getByPlaceholderText('Display name'), 'My New Query Name' ); // Click on Save in the Dropdown await userEvent.click(screen.getByRole('button', {name: 'Save for Org'})); // The banner should not render expect(screen.queryByText('Discover Trends')).not.toBeInTheDocument(); }); it('saves a well-formed query', async () => { mount(location, organization, router, errorsView, undefined, yAxis); // Click on ButtonSaveAs to open dropdown await userEvent.click(screen.getByRole('button', {name: 'Save as'})); // Fill in the Input await userEvent.type( screen.getByPlaceholderText('Display name'), 'My New Query Name' ); // Click on Save in the Dropdown await userEvent.click(screen.getByRole('button', {name: 'Save for Org'})); expect(mockUtils).toHaveBeenCalledWith( expect.anything(), // api organization, expect.objectContaining({ ...errorsView, name: 'My New Query Name', }), yAxis, true ); }); it('rejects if query.name is empty', async () => { mount(location, organization, router, errorsView, undefined, yAxis); // Click on ButtonSaveAs to open dropdown await userEvent.click(screen.getByRole('button', {name: 'Save as'})); // Do not fill in Input // Click on Save in the Dropdown await userEvent.click(screen.getByRole('button', {name: 'Save for Org'})); // Check that EventView has a name expect(errorsView.name).toBe('Errors by Title'); expect(mockUtils).not.toHaveBeenCalled(); }); }); describe('viewing a saved query', () => { let mockUtils; beforeEach(() => { mockUtils = jest .spyOn(utils, 'handleDeleteQuery') .mockImplementation(() => Promise.resolve()); }); afterEach(() => { mockUtils.mockClear(); }); it('renders the correct set of buttons', async () => { mount( location, organization, router, EventView.fromSavedQuery({...errorsQuery, yAxis}), savedQuery, yAxis ); expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument(); expect( screen.queryByRole('button', {name: /save changes/i}) ).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.getByRole('menuitemradio', {name: /delete saved query/i}) ).toBeInTheDocument(); }); it('treats undefined yAxis the same as count() when checking for changes', async () => { mount( location, organization, router, errorsViewSaved, {...savedQuery, yAxis: undefined}, ['count()'] ); expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument(); expect( screen.queryByRole('button', {name: /save changes/i}) ).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.getByRole('menuitemradio', {name: /delete saved query/i}) ).toBeInTheDocument(); }); it('converts string yAxis values to array when checking for changes', async () => { mount( location, organization, router, errorsViewSaved, {...savedQuery, yAxis: 'count()'}, ['count()'] ); expect(screen.queryByRole('button', {name: /save as/i})).not.toBeInTheDocument(); expect( screen.queryByRole('button', {name: /save changes/i}) ).not.toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.getByRole('menuitemradio', {name: /delete saved query/i}) ).toBeInTheDocument(); }); it('deletes the saved query', async () => { mount(location, organization, router, errorsViewSaved, savedQuery, yAxis); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); await userEvent.click( screen.getByRole('menuitemradio', {name: /delete saved query/i}) ); expect(mockUtils).toHaveBeenCalledWith( expect.anything(), // api organization, expect.objectContaining({id: '1'}) ); }); }); describe('modifying a saved query', () => { let mockUtils; it('renders the correct set of buttons', async () => { mount( location, organization, router, errorsViewModified, errorsViewSaved.toNewQuery(), yAxis ); expect(screen.queryByRole('button', {name: /save as/i})).toBeInTheDocument(); expect(screen.getByRole('button', {name: /save changes/i})).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', {name: /discover context menu/i})); expect( screen.getByRole('menuitemradio', {name: /delete saved query/i}) ).toBeInTheDocument(); }); describe('updates the saved query', () => { beforeEach(() => { mockUtils = jest .spyOn(utils, 'handleUpdateQuery') .mockImplementation(() => Promise.resolve(savedQuery)); }); afterEach(() => { mockUtils.mockClear(); }); it('accepts a well-formed query', async () => { const mockSetSavedQuery = jest.fn(); mount( location, organization, router, errorsViewModified, savedQuery, yAxis, false, mockSetSavedQuery ); // Click on Save in the Dropdown await userEvent.click(screen.getByRole('button', {name: /save changes/i})); await waitFor(() => { expect(mockUtils).toHaveBeenCalledWith( expect.anything(), // api organization, expect.objectContaining({ ...errorsViewModified, }), yAxis ); expect(mockSetSavedQuery).toHaveBeenCalled(); }); }); }); describe('creates a separate query', () => { beforeEach(() => { mockUtils = jest .spyOn(utils, 'handleCreateQuery') .mockImplementation(() => Promise.resolve(savedQuery)); }); afterEach(() => { mockUtils.mockClear(); }); it('checks that it is forked from a saved query', async () => { mount(location, organization, router, errorsViewModified, savedQuery, yAxis); // Click on ButtonSaveAs to open dropdown await userEvent.click(screen.getByRole('button', {name: 'Save as'})); // Fill in the Input await userEvent.type(screen.getByPlaceholderText('Display name'), 'Forked Query'); // Click on Save in the Dropdown await userEvent.click(screen.getByRole('button', {name: 'Save for Org'})); expect(mockUtils).toHaveBeenCalledWith( expect.anything(), // api organization, expect.objectContaining({ ...errorsViewModified, name: 'Forked Query', }), yAxis, false ); }); }); }); describe('create alert from discover', () => { it('renders create alert button when metrics alerts is enabled', () => { const metricAlertOrg = { ...organization, features: ['incidents'], }; mount(location, metricAlertOrg, router, errorsViewModified, savedQuery, yAxis); expect(screen.getByRole('button', {name: /create alert/i})).toBeInTheDocument(); }); it('does not render create alert button without metric alerts', () => { mount(location, organization, router, errorsViewModified, savedQuery, yAxis); expect( screen.queryByRole('button', {name: /create alert/i}) ).not.toBeInTheDocument(); }); }); });