import {browserHistory} from 'react-router'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {act} from 'sentry-test/reactTestingLibrary'; import * as pageFilters from 'sentry/actionCreators/pageFilters'; import OrganizationStore from 'sentry/stores/organizationStore'; import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {OrganizationContext} from 'sentry/views/organizationContext'; import PerformanceContent from 'sentry/views/performance/content'; import {DEFAULT_MAX_DURATION} from 'sentry/views/performance/trends/utils'; import {RouteContext} from 'sentry/views/routeContext'; const FEATURES = ['performance-view']; function WrappedComponent({organization, isMEPEnabled = false, router}) { const client = new QueryClient(); return ( ); } function initializeData(projects, query, features = FEATURES) { const organization = TestStubs.Organization({ features, projects, }); const initialData = initializeOrg({ organization, router: { location: { pathname: '/test', query: query || {}, }, }, }); act(() => void OrganizationStore.onUpdate(initialData.organization, {replace: true})); act(() => ProjectsStore.loadInitialData(initialData.organization.projects)); return initialData; } function initializeTrendsData(query, addDefaultQuery = true) { const projects = [ TestStubs.Project({id: '1', firstTransactionEvent: false}), TestStubs.Project({id: '2', firstTransactionEvent: true}), ]; const organization = TestStubs.Organization({ FEATURES, projects, }); const otherTrendsQuery = addDefaultQuery ? { query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`, } : {}; const initialData = initializeOrg({ organization, router: { location: { pathname: '/test', query: { ...otherTrendsQuery, ...query, }, }, }, }); act(() => ProjectsStore.loadInitialData(initialData.organization.projects)); return initialData; } describe('Performance > Content', function () { enforceActOnUseLegacyStoreHook(); beforeEach(function () { act(() => void TeamStore.loadInitialData([], false, null)); browserHistory.push = jest.fn(); jest.spyOn(pageFilters, 'updateDateTime'); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-stats/', body: {data: [[123, []]]}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-histogram/', body: {'transaction.duration': [{bin: 0, count: 1000}]}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/users/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', method: 'POST', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/sdk-updates/', body: [], }); MockApiClient.addMockResponse({ url: '/prompts-activity/', body: {}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/eventsv2/', body: { meta: { user: 'string', transaction: 'string', 'project.id': 'integer', tpm: 'number', p50: 'number', p95: 'number', failure_rate: 'number', apdex_300: 'number', count_unique_user: 'number', count_miserable_user_300: 'number', user_misery_300: 'number', }, data: [ { transaction: '/apple/cart', 'project.id': 1, user: 'uhoh@example.com', tpm: 30, p50: 100, p95: 500, failure_rate: 0.1, apdex_300: 0.6, count_unique_user: 1000, count_miserable_user_300: 122, user_misery_300: 0.114, }, ], }, match: [ (_, options) => { if (!options.hasOwnProperty('query')) { return false; } if (!options.query.hasOwnProperty('field')) { return false; } return !options.query.field.includes('team_key_transaction'); }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/eventsv2/', body: { meta: { user: 'string', transaction: 'string', 'project.id': 'integer', tpm: 'number', p50: 'number', p95: 'number', failure_rate: 'number', apdex_300: 'number', count_unique_user: 'number', count_miserable_user_300: 'number', user_misery_300: 'number', }, data: [ { team_key_transaction: 1, transaction: '/apple/cart', 'project.id': 1, user: 'uhoh@example.com', tpm: 30, p50: 100, p95: 500, failure_rate: 0.1, apdex_300: 0.6, count_unique_user: 1000, count_miserable_user_300: 122, user_misery_300: 0.114, }, { team_key_transaction: 0, transaction: '/apple/checkout', 'project.id': 1, user: 'uhoh@example.com', tpm: 30, p50: 100, p95: 500, failure_rate: 0.1, apdex_300: 0.6, count_unique_user: 1000, count_miserable_user_300: 122, user_misery_300: 0.114, }, ], }, match: [ (_, options) => { if (!options.hasOwnProperty('query')) { return false; } if (!options.query.hasOwnProperty('field')) { return false; } return options.query.field.includes('team_key_transaction'); }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-meta/', body: { count: 2, }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-trends/', body: { stats: {}, events: {meta: {}, data: []}, }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-trends-stats/', body: { stats: {}, events: {meta: {}, data: []}, }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-vitals/', body: { 'measurements.lcp': { poor: 1, meh: 2, good: 3, total: 6, p75: 4500, }, }, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/key-transactions-list/`, body: [], }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/events/`, body: { data: [{}], meta: {}, }, }); }); afterEach(function () { MockApiClient.clearMockResponses(); act(() => ProjectsStore.reset()); pageFilters.updateDateTime.mockRestore(); }); it('renders basic UI elements', async function () { const projects = [TestStubs.Project({firstTransactionEvent: true})]; const data = initializeData(projects, {}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); // performance landing container expect(wrapper.find('div[data-test-id="performance-landing-v3"]').exists()).toBe( true ); // No onboarding should show. expect(wrapper.find('Onboarding')).toHaveLength(0); // Table should render. expect(wrapper.find('Table')).toHaveLength(1); wrapper.unmount(); }); it('renders onboarding state when the selected project has no events', async function () { const projects = [ TestStubs.Project({id: 1, firstTransactionEvent: false}), TestStubs.Project({id: 2, firstTransactionEvent: true}), ]; const data = initializeData(projects, {project: [1]}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); // onboarding should show. expect(wrapper.find('Onboarding')).toHaveLength(1); // Table should not show. expect(wrapper.find('Table')).toHaveLength(0); wrapper.unmount(); }); it('does not render onboarding for "my projects"', async function () { const projects = [ TestStubs.Project({id: '1', firstTransactionEvent: false}), TestStubs.Project({id: '2', firstTransactionEvent: true}), ]; const data = initializeData(projects, {project: ['-1']}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); expect(wrapper.find('Onboarding')).toHaveLength(0); wrapper.unmount(); }); it('forwards conditions to transaction summary', async function () { const projects = [TestStubs.Project({id: '1', firstTransactionEvent: true})]; const data = initializeData(projects, {project: ['1'], query: 'sentry:yes'}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); const link = wrapper.find('[data-test-id="grid-editable"] GridBody Link').at(0); link.simulate('click', {button: 0}); expect(data.router.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ transaction: '/apple/cart', query: 'sentry:yes', }), }) ); wrapper.unmount(); }); it('Default period for trends does not call updateDateTime', async function () { const data = initializeTrendsData({query: 'tag:value'}, false); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); expect(pageFilters.updateDateTime).toHaveBeenCalledTimes(0); wrapper.unmount(); }); it('Navigating to trends does not modify statsPeriod when already set', async function () { const data = initializeTrendsData({ query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION} api`, statsPeriod: '24h', }); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); const trendsLink = wrapper.find('[data-test-id="landing-header-trends"]').at(0); trendsLink.simulate('click'); expect(pageFilters.updateDateTime).toHaveBeenCalledTimes(0); expect(browserHistory.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/performance/trends/', query: { query: `tpm():>0.005 transaction.duration:>10 transaction.duration:<${DEFAULT_MAX_DURATION}`, statsPeriod: '24h', }, }) ); wrapper.unmount(); }); it('Default page (transactions) without trends feature will not update filters if none are set', async function () { const projects = [ TestStubs.Project({id: 1, firstTransactionEvent: false}), TestStubs.Project({id: 2, firstTransactionEvent: true}), ]; const data = initializeData(projects, {view: undefined}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); expect(browserHistory.push).toHaveBeenCalledTimes(0); wrapper.unmount(); }); it('Default page (transactions) with trends feature will not update filters if none are set', async function () { const data = initializeTrendsData({view: undefined}, false); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); expect(browserHistory.push).toHaveBeenCalledTimes(0); wrapper.unmount(); }); it('Tags are replaced with trends default query if navigating to trends', async function () { const data = initializeTrendsData({query: 'device.family:Mac'}, false); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); const trendsLink = wrapper.find('[data-test-id="landing-header-trends"]').at(0); trendsLink.simulate('click'); expect(browserHistory.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/performance/trends/', query: { query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`, }, }) ); wrapper.unmount(); }); it('Display Create Sample Transaction Button', async function () { const projects = [ TestStubs.Project({id: 1, firstTransactionEvent: false}), TestStubs.Project({id: 2, firstTransactionEvent: false}), ]; const data = initializeData(projects, {view: undefined}); const wrapper = mountWithTheme( , data.routerContext ); await act(async () => { await tick(); wrapper.update(); }); expect( wrapper.find('Button[data-test-id="create-sample-transaction-btn"]').exists() ).toBe(true); wrapper.unmount(); }); });