123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
- import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
- import type {NewQuery, Organization, SavedQuery} from 'sentry/types/organization';
- import EventView from 'sentry/utils/discover/eventView';
- import {DisplayModes, SavedQueryDatasets} from 'sentry/utils/discover/types';
- import {WidgetType} from 'sentry/views/dashboards/types';
- import {getAllViews} 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(
- <SavedQueryButtonGroup
- location={location}
- organization={organization}
- eventView={eventView}
- savedQuery={savedQuery}
- disabled={disabled}
- updateCallback={() => {}}
- yAxis={yAxis}
- router={router}
- queryDataLoading={false}
- setSavedQuery={setSavedQuery}
- setHomepageQuery={jest.fn()}
- />
- );
- }
- describe('Discover > SaveQueryButtonGroup', function () {
- let organization: Organization;
- let errorsView: EventView;
- let savedQuery: SavedQuery;
- let errorsViewSaved: EventView;
- let errorsViewModified: EventView;
- let errorsQuery: NewQuery;
- const location = {
- pathname: '/organization/eventsv2/',
- query: {},
- };
- const router = {
- location: {query: {}},
- };
- const yAxis = ['count()', 'failure_count()'];
- beforeEach(() => {
- organization = OrganizationFixture({
- features: ['discover-query', 'dashboards-edit'],
- });
- errorsQuery = {
- ...(getAllViews(organization).find(
- view => view.name === 'Errors by Title'
- ) as NewQuery),
- yAxis: ['count()'],
- display: DisplayModes.DEFAULT,
- };
- errorsView = EventView.fromSavedQuery(errorsQuery);
- errorsViewSaved = EventView.fromSavedQuery(errorsQuery);
- errorsViewSaved.id = '1';
- errorsViewModified = EventView.fromSavedQuery(errorsQuery);
- errorsViewModified.id = '1';
- errorsViewModified.name = 'Modified Name';
- savedQuery = {
- ...errorsViewSaved.toNewQuery(),
- yAxis,
- dateCreated: '',
- dateUpdated: '',
- id: '1',
- };
- });
- 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('opens dashboard modal with the right props', async () => {
- organization = OrganizationFixture({
- features: [
- 'discover-query',
- 'dashboards-edit',
- 'performance-discover-dataset-selector',
- ],
- });
- mount(
- location,
- organization,
- router,
- errorsView,
- {...savedQuery, queryDataset: SavedQueryDatasets.ERRORS},
- yAxis
- );
- expect(
- screen.getByRole('button', {name: /discover context menu/i})
- ).toBeInTheDocument();
- await userEvent.click(screen.getByRole('button', {name: /discover context menu/i}));
- expect(
- screen.queryByRole('menuitemradio', {name: /add to dashboard/i})
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('menuitemradio', {name: /add to dashboard/i})
- );
- expect(openAddToDashboardModal).toHaveBeenCalledWith(
- expect.objectContaining({
- widget: {
- displayType: 'line',
- interval: undefined,
- limit: undefined,
- queries: [
- {
- aggregates: ['count()', 'failure_count()'],
- columns: [],
- conditions: 'event.type:error',
- fields: ['count()', 'failure_count()'],
- name: '',
- orderby: '-count()',
- },
- ],
- title: 'Errors by Title',
- widgetType: WidgetType.ERRORS,
- },
- widgetAsQueryParams: expect.objectContaining({
- dataset: WidgetType.ERRORS,
- defaultTableColumns: ['title', 'count()', 'count_unique(user)', 'project'],
- defaultTitle: 'Errors by Title',
- defaultWidgetQuery:
- 'name=&aggregates=count()%2Cfailure_count()&columns=&fields=count()%2Cfailure_count()&conditions=event.type%3Aerror&orderby=-count()',
- displayType: 'line',
- end: undefined,
- limit: undefined,
- source: 'discoverv2',
- start: undefined,
- statsPeriod: '24h',
- }),
- })
- );
- });
- 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: jest.SpyInstance;
- 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: jest.SpyInstance;
- 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();
- });
- it('uses the throughput alert type for transaction queries', () => {
- const metricAlertOrg = {
- ...organization,
- features: ['incidents', 'performance-discover-dataset-selector'],
- };
- const transactionSavedQuery = {
- ...savedQuery,
- queryDataset: SavedQueryDatasets.TRANSACTIONS,
- query: 'foo:bar',
- };
- const transactionView = EventView.fromSavedQuery(transactionSavedQuery);
- mount(
- location,
- metricAlertOrg,
- router,
- transactionView,
- transactionSavedQuery,
- yAxis
- );
- const createAlertButton = screen.getByRole('button', {name: /create alert/i});
- const href = createAlertButton.getAttribute('href')!;
- const queryParameters = new URLSearchParams(href.split('?')[1]);
- expect(queryParameters.get('query')).toEqual(
- '(foo:bar) AND (event.type:transaction)'
- );
- expect(queryParameters.get('dataset')).toEqual('transactions');
- expect(queryParameters.get('eventTypes')).toEqual('transaction');
- });
- it('uses the num errors alert type for error queries', () => {
- const metricAlertOrg = {
- ...organization,
- features: ['incidents', 'performance-discover-dataset-selector'],
- };
- const errorSavedQuery = {
- ...savedQuery,
- queryDataset: SavedQueryDatasets.ERRORS,
- query: 'foo:bar',
- };
- const transactionView = EventView.fromSavedQuery(errorSavedQuery);
- mount(location, metricAlertOrg, router, transactionView, errorSavedQuery, yAxis);
- const createAlertButton = screen.getByRole('button', {name: /create alert/i});
- const href = createAlertButton.getAttribute('href')!;
- const queryParameters = new URLSearchParams(href.split('?')[1]);
- expect(queryParameters.get('query')).toEqual('foo:bar');
- expect(queryParameters.get('dataset')).toEqual('events');
- expect(queryParameters.get('eventTypes')).toEqual('error');
- });
- });
- });
|