import {browserHistory} from 'react-router'; import {addMetricsDataMock} from 'sentry-test/performance/addMetricsDataMock'; import {initializeData} from 'sentry-test/performance/initializePerformanceData'; import { act, render, screen, userEvent, waitFor, waitForElementToBeRemoved, } from 'sentry-test/reactTestingLibrary'; import TeamStore from 'sentry/stores/teamStore'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import {QueryClient, QueryClientProvider} from 'sentry/utils/queryClient'; import {OrganizationContext} from 'sentry/views/organizationContext'; import {generatePerformanceEventView} from 'sentry/views/performance/data'; import {PerformanceLanding} from 'sentry/views/performance/landing'; import {REACT_NATIVE_COLUMN_TITLES} from 'sentry/views/performance/landing/data'; import {LandingDisplayField} from 'sentry/views/performance/landing/utils'; const searchHandlerMock = jest.fn(); function WrappedComponent({data, withStaticFilters = false}) { const eventView = generatePerformanceEventView( data.router.location, data.projects, { withStaticFilters, }, data.organization ); const client = new QueryClient(); return ( {}} setError={() => {}} withStaticFilters={withStaticFilters} /> ); } describe('Performance > Landing > Index', function () { let eventStatsMock: jest.Mock; let eventsMock: jest.Mock; let wrapper: any; act(() => void TeamStore.loadInitialData([], false, null)); beforeEach(function () { // eslint-disable-next-line no-console jest.spyOn(console, 'error').mockImplementation(jest.fn()); MockApiClient.addMockResponse({ url: '/organizations/org-slug/sdk-updates/', body: [], }); MockApiClient.addMockResponse({ url: '/prompts-activity/', body: {}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/key-transactions-list/`, body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/legacy-key-transactions-count/`, body: [], }); eventStatsMock = MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events-stats/`, body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events-trends-stats/`, body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events-histogram/`, body: [], }); eventsMock = MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events/`, body: { meta: { fields: { id: 'string', }, }, data: [ { id: '1234', }, ], }, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/metrics-compatibility/`, body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/metrics-compatibility-sums/`, body: [], }); }); afterEach(function () { MockApiClient.clearMockResponses(); jest.clearAllMocks(); jest.restoreAllMocks(); if (wrapper) { wrapper.unmount(); wrapper = undefined; } }); it('renders basic UI elements', function () { const data = initializeData(); wrapper = render(, data.routerContext); expect(screen.getByTestId('performance-landing-v3')).toBeInTheDocument(); }); it('renders frontend pageload view', function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument(); expect(screen.getByTestId('performance-table')).toBeInTheDocument(); const titles = screen.getAllByTestId('performance-widget-title'); expect(titles).toHaveLength(5); expect(titles[0]).toHaveTextContent('Worst LCP Web Vitals'); expect(titles[1]).toHaveTextContent('Worst FCP Web Vitals'); expect(titles[2]).toHaveTextContent('p75 LCP'); expect(titles[3]).toHaveTextContent('LCP Distribution'); expect(titles[4]).toHaveTextContent('FCP Distribution'); }); it('renders frontend other view', function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.FRONTEND_OTHER}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('performance-table')).toBeInTheDocument(); }); it('renders backend view', function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.BACKEND}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('performance-table')).toBeInTheDocument(); }); it('renders mobile view', function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.MOBILE}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('performance-table')).toBeInTheDocument(); }); it('renders react-native table headers in mobile view', async function () { const project = TestStubs.Project({platform: 'react-native'}); const projects = [project]; const data = initializeData({ query: {landingDisplay: LandingDisplayField.MOBILE}, selectedProject: project.id, projects, }); wrapper = render(, data.routerContext); expect(await screen.findByTestId('performance-table')).toBeInTheDocument(); expect(screen.getByTestId('grid-editable')).toBeInTheDocument(); const columnHeaders = await screen.findAllByTestId('grid-head-cell'); expect(columnHeaders).toHaveLength(REACT_NATIVE_COLUMN_TITLES.length); for (const [index, title] of columnHeaders.entries()) { expect(title).toHaveTextContent(REACT_NATIVE_COLUMN_TITLES[index]); } }); it('renders all transactions view', async function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.ALL}, }); wrapper = render(, data.routerContext); expect(await screen.findByTestId('performance-table')).toBeInTheDocument(); await waitFor(() => expect(eventStatsMock).toHaveBeenCalledTimes(1)); // Only one request is made since the query batcher is working. expect(eventStatsMock).toHaveBeenNthCalledWith( 1, expect.anything(), expect.objectContaining({ query: expect.objectContaining({ environment: [], interval: '1h', partial: '1', project: [], query: 'event.type:transaction', referrer: 'api.performance.generic-widget-chart.user-misery-area', statsPeriod: '14d', yAxis: ['user_misery()', 'tpm()', 'failure_rate()'], }), }) ); expect(eventsMock).toHaveBeenCalledTimes(2); const titles = await screen.findAllByTestId('performance-widget-title'); expect(titles).toHaveLength(5); expect(titles.at(0)).toHaveTextContent('Most Regressed'); expect(titles.at(1)).toHaveTextContent('Most Improved'); expect(titles.at(2)).toHaveTextContent('User Misery'); expect(titles.at(3)).toHaveTextContent('Transactions Per Minute'); expect(titles.at(4)).toHaveTextContent('Failure Rate'); }); it('Can switch between landing displays', async function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD, abc: '123'}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument(); await userEvent.click(screen.getByRole('tab', {name: 'All Transactions'})); expect(browserHistory.push).toHaveBeenNthCalledWith( 1, expect.objectContaining({ pathname: data.location.pathname, query: {query: '', abc: '123'}, }) ); }); it('Updating projects switches performance view', function () { const data = initializeData({ query: {landingDisplay: LandingDisplayField.FRONTEND_PAGELOAD}, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument(); const updatedData = initializeData({ projects: [TestStubs.Project({id: 123, platform: 'unknown'})], selectedProject: 123, }); wrapper.rerender(, data.routerContext); expect(screen.getByTestId('all-transactions-view')).toBeInTheDocument(); }); it('View correctly defaults based on project without url param', function () { const data = initializeData({ projects: [TestStubs.Project({id: 99, platform: 'javascript-react'})], selectedProject: 99, }); wrapper = render(, data.routerContext); expect(screen.getByTestId('frontend-pageload-view')).toBeInTheDocument(); }); describe('With transaction search feature', function () { it('does not search for empty string transaction', async function () { const data = initializeData(); render(, data.routerContext); await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); await userEvent.type(screen.getByPlaceholderText('Search Transactions'), '{enter}'); expect(searchHandlerMock).toHaveBeenCalledWith('', 'transactionsOnly'); }); it('renders the search bar', async function () { addMetricsDataMock(); const data = initializeData({ query: { field: 'test', }, }); wrapper = render( , data.routerContext ); expect(await screen.findByTestId('transaction-search-bar')).toBeInTheDocument(); }); it('extracts free text from the query', async function () { const data = initializeData(); wrapper = render(, data.routerContext); await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator')); expect(await screen.findByPlaceholderText('Search Transactions')).toHaveValue(''); }); }); describe('With span operations widget feature flag', function () { it('Displays the span operations widget', async function () { addMetricsDataMock(); const data = initializeData({ features: [ 'performance-transaction-name-only-search', 'performance-new-widget-designs', ], }); wrapper = render(, data.routerContext); const titles = await screen.findAllByTestId('performance-widget-title'); expect(titles.at(0)).toHaveTextContent('Most Regressed'); }); }); });