import type {ComponentProps} from 'react'; import {Fragment} from 'react'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {SearchFixture} from 'sentry-fixture/search'; import { render, renderGlobalModal, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import GlobalModalContainer from 'sentry/components/globalModal'; import {SavedSearchVisibility} from 'sentry/types/group'; import localStorageWrapper from 'sentry/utils/localStorage'; import SavedIssueSearches from 'sentry/views/issueList/savedIssueSearches'; import {SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY} from 'sentry/views/issueList/utils'; describe('SavedIssueSearches', function () { const organization = OrganizationFixture(); const recommendedSearch = SearchFixture({ id: 'global-search', isGlobal: true, name: 'Assigned to Me', query: 'is:unresolved assigned:me', visibility: SavedSearchVisibility.ORGANIZATION, }); const userSearch = SearchFixture({ id: 'user-search', isGlobal: false, name: 'Just Firefox', query: 'browser:firefox', visibility: SavedSearchVisibility.OWNER, }); const orgSearch = SearchFixture({ id: 'org-search', isGlobal: false, name: 'Last 4 Hours', query: 'age:-4h', visibility: SavedSearchVisibility.ORGANIZATION, sort: 'date', }); const pinnedSearch = SearchFixture({ id: 'pinned-search', isGlobal: false, isPinned: true, name: 'My Pinned Search', query: 'age:-4h', }); const defaultProps: ComponentProps = { organization, onSavedSearchSelect: jest.fn(), query: 'is:unresolved', sort: 'date', }; beforeEach(() => { localStorageWrapper.setItem(SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY, 'true'); MockApiClient.clearMockResponses(); jest.clearAllMocks(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', body: [], }); }); it('displays saved searches with correct text and in correct sections', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [userSearch, recommendedSearch, orgSearch, pinnedSearch], }); render(); await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); it('hides saves searches by default past first 4', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [...new Array(6)].map((_, i) => ({ ...orgSearch, name: 'Test Search', id: i, })), }); render(); expect(await screen.findAllByText('Test Search')).toHaveLength(4); await userEvent.click(screen.getByRole('button', {name: /show 2 more/i})); expect(screen.getAllByText('Test Search')).toHaveLength(6); }); it('can select a saved search', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch, orgSearch, pinnedSearch], }); render(); await userEvent.click(await screen.findByRole('button', {name: 'Assigned to Me'})); expect(defaultProps.onSavedSearchSelect).toHaveBeenLastCalledWith(recommendedSearch); await userEvent.click(screen.getByRole('button', {name: 'Last 4 Hours'})); expect(defaultProps.onSavedSearchSelect).toHaveBeenLastCalledWith(orgSearch); }); it('does not show header when there are no org saved searches', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch], }); render(); expect( await screen.findByText(/You don't have any saved searches/i) ).toBeInTheDocument(); }); it('does not show overflow menu for recommended searches', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch], }); render(); await waitFor(() => { expect( screen.queryByRole('button', {name: /saved search options/i}) ).not.toBeInTheDocument(); }); }); it('can delete an org saved search with correct permissions', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch, orgSearch, pinnedSearch], }); const deleteMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/org-search/', method: 'DELETE', }); render(); renderGlobalModal(); await userEvent.click( await screen.findByRole('button', {name: /saved search options/i}) ); await userEvent.click(screen.getByRole('menuitemradio', {name: /delete/i})); const modal = screen.getByRole('dialog'); expect(within(modal).getByText(/are you sure/i)).toBeInTheDocument(); await userEvent.click(within(modal).getByRole('button', {name: /confirm/i})); await waitFor(() => { expect(deleteMock).toHaveBeenCalledTimes(1); expect(screen.queryByText(orgSearch.name)).not.toBeInTheDocument(); }); }); it('can edit an org saved search with correct permissions', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch, orgSearch, pinnedSearch], }); const putMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/org-search/', method: 'PUT', body: { ...orgSearch, name: 'new name', }, }); render( ); await userEvent.click( await screen.findByRole('button', {name: /saved search options/i}) ); await userEvent.click(screen.getByRole('menuitemradio', {name: /edit/i})); const modal = screen.getByRole('dialog'); await userEvent.clear(within(modal).getByRole('textbox', {name: /name/i})); await userEvent.type(within(modal).getByRole('textbox', {name: /name/i}), 'new name'); await userEvent.click(within(modal).getByRole('button', {name: /save/i})); await waitFor(() => { expect(putMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: expect.objectContaining({ name: 'new name', }), }) ); expect(screen.getByText('new name')).toBeInTheDocument(); }); }); it('cannot delete or edit a saved search without correct permissions', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch, orgSearch, pinnedSearch], }); render( access !== 'org:write'), }} /> ); await userEvent.click( await screen.findByRole('button', {name: /saved search options/i}) ); expect( screen.getByText('You do not have permission to delete this search.') ).toBeInTheDocument(); expect( screen.getByText('You do not have permission to edit this search.') ).toBeInTheDocument(); }); it('can create a new saved search', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', body: [recommendedSearch], }); const mockSave = MockApiClient.addMockResponse({ url: '/organizations/org-slug/searches/', method: 'POST', body: {}, }); render(); renderGlobalModal(); await userEvent.click(await screen.findByRole('button', {name: /add saved search/i})); const modal = screen.getByRole('dialog'); await userEvent.type( within(modal).getByRole('textbox', {name: /name/i}), 'new saved search' ); await userEvent.click(within(modal).getByRole('button', {name: /save/i})); await waitFor(() => { expect(mockSave).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: expect.objectContaining({ name: 'new saved search', query: 'is:unresolved', }), }) ); }); // Modal should close await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); });