import {browserHistory} from 'react-router'; import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {act} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import {OrganizationContext} from 'sentry/views/organizationContext'; import TransactionVitals from 'sentry/views/performance/transactionSummary/transactionVitals'; import { VITAL_GROUPS, ZOOM_KEYS, } from 'sentry/views/performance/transactionSummary/transactionVitals/constants'; function initialize({project, features, transaction, query} = {}) { features = features || ['performance-view']; project = project || TestStubs.Project(); query = query || {}; const data = initializeOrg({ organization: TestStubs.Organization({ features, projects: [project], }), router: { location: { query: { transaction: transaction || '/', project: project.id, ...query, }, }, }, }); act(() => ProjectsStore.loadInitialData(data.organization.projects)); return data; } const WrappedComponent = ({organization, ...props}) => { return ( ); }; /** * These values are what we expect to see on the page based on the * mocked api responses below. */ const vitals = [ { slug: 'fp', heading: 'First Paint (FP)', baseline: '4.57s', }, { slug: 'fcp', heading: 'First Contentful Paint (FCP)', baseline: '1.46s', }, { slug: 'lcp', heading: 'Largest Contentful Paint (LCP)', baseline: '1.34s', }, { slug: 'fid', heading: 'First Input Delay (FID)', baseline: '987.00ms', }, { slug: 'cls', heading: 'Cumulative Layout Shift (CLS)', baseline: '0.02', }, ]; describe('Performance > Web Vitals', function () { enforceActOnUseLegacyStoreHook(); beforeEach(function () { // @ts-ignore no-console // eslint-disable-next-line no-console jest.spyOn(console, 'error').mockImplementation(jest.fn()); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/project-transaction-threshold-override/', method: 'GET', body: { threshold: '800', metric: 'lcp', }, }); // Mock baseline measurements MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-vitals/', body: { 'measurements.fp': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567}, 'measurements.fcp': {poor: 1, meh: 2, good: 3, total: 6, p75: 1456}, 'measurements.lcp': {poor: 1, meh: 2, good: 3, total: 6, p75: 1342}, 'measurements.fid': {poor: 1, meh: 2, good: 3, total: 6, p75: 987}, 'measurements.cls': {poor: 1, meh: 2, good: 3, total: 6, p75: 0.02}, }, }); const histogramData = {}; const webVitals = VITAL_GROUPS.reduce((vs, group) => vs.concat(group.vitals), []); for (const measurement of webVitals) { const data = []; for (let i = 0; i < 100; i++) { data.push({ histogram: i, count: i, }); } histogramData[`measurements.${measurement}`] = data; } MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-histogram/', body: histogramData, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/key-transactions-list/`, body: [], }); MockApiClient.addMockResponse({ url: '/prompts-activity/', body: {}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/sdk-updates/', body: [], }); }); afterEach(() => { // @ts-ignore no-console // eslint-disable-next-line no-console console.error.mockRestore(); }); it('render no access without feature', async function () { const {organization, router} = initialize({ features: [], }); const wrapper = mountWithTheme( ); await tick(); wrapper.update(); expect(wrapper.text()).toEqual("You don't have access to this feature"); }); it('renders the basic UI components', async function () { const {organization, router, routerContext} = initialize(); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.find('TransactionHeader')).toHaveLength(1); expect(wrapper.find('SearchBar')).toHaveLength(1); expect(wrapper.find('TransactionVitals')).toHaveLength(1); }); it('renders the correct bread crumbs', async function () { const {organization, router, routerContext} = initialize(); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.find('Breadcrumb').text()).toEqual( expect.stringContaining('Web Vitals') ); }); it('renders all vitals cards correctly', async function () { const {organization, router, routerContext} = initialize(); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); const vitalCards = wrapper.find('VitalCard'); expect(vitalCards).toHaveLength(5); vitalCards.forEach((vitalCard, i) => { expect(vitalCard.find('CardSectionHeading').text()).toEqual( expect.stringContaining(vitals[i].heading) ); expect(vitalCard.find('StatNumber').text()).toEqual(vitals[i].baseline); }); expect(vitalCards.find('BarChart')).toHaveLength(5); }); describe('reset view', function () { it('disables button on default view', async function () { const {organization, router, routerContext} = initialize(); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect( wrapper.find('Button[data-test-id="reset-view"]').prop('disabled') ).toBeTruthy(); }); it('enables button on left zoom', async function () { const {organization, router, routerContext} = initialize({ query: { lcpStart: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect( wrapper.find('Button[data-test-id="reset-view"]').prop('disabled') ).toBeFalsy(); }); it('enables button on right zoom', async function () { const {organization, router, routerContext} = initialize({ query: { fpEnd: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect( wrapper.find('Button[data-test-id="reset-view"]').prop('disabled') ).toBeFalsy(); }); it('enables button on left and right zoom', async function () { const {organization, router, routerContext} = initialize({ query: { fcpStart: '20', fcpEnd: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect( wrapper.find('Button[data-test-id="reset-view"]').prop('disabled') ).toBeFalsy(); }); it('resets view properly', async function () { const {organization, router, routerContext} = initialize({ query: { fidStart: '20', lcpEnd: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); wrapper.find('Button[data-test-id="reset-view"]').simulate('click'); expect(browserHistory.push).toHaveBeenCalledWith({ query: expect.not.objectContaining( ZOOM_KEYS.reduce((obj, key) => { obj[key] = expect.anything(); return obj; }, {}) ), }); }); it('renders an info alert when missing web vitals data', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-vitals/', body: { 'measurements.fp': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567}, 'measurements.fcp': {poor: 1, meh: 2, good: 3, total: 6, p75: 1456}, }, }); const {organization, router, routerContext} = initialize({ query: { lcpStart: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.find('Alert')).toHaveLength(1); }); it('does not render an info alert when data from all web vitals is present', async function () { const {organization, router, routerContext} = initialize({ query: { lcpStart: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.find('Alert')).toHaveLength(0); }); }); it('renders an info alert when some web vitals measurements has no data available', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-vitals/', body: { 'measurements.cls': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567}, 'measurements.fcp': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567}, 'measurements.fid': {poor: 1, meh: 2, good: 3, total: 6, p75: 4567}, 'measurements.fp': {poor: 1, meh: 2, good: 3, total: 6, p75: 1456}, 'measurements.lcp': {poor: 0, meh: 0, good: 0, total: 0, p75: null}, }, }); const {organization, router, routerContext} = initialize({ query: { lcpStart: '20', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.find('Alert')).toHaveLength(1); }); });