import {browserHistory} from 'react-router'; import {Location, Query} from 'history'; import {Organization} from 'sentry-fixture/organization'; import {initializeOrg} from 'sentry-test/initializeOrg'; import { act, render, screen, userEvent, waitForElementToBeRemoved, } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import type {Organization as TOrganization, Project} from 'sentry/types'; 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'; interface HistogramData { count: number; histogram: number; } function initialize({ project, features, transaction, query, }: { features?: string[]; project?: Project; query?: Query; transaction?: string; } = {}) { features = features || ['performance-view']; project = project || TestStubs.Project(); query = query || {}; const data = initializeOrg({ organization: Organization({ features, projects: project ? [project] : [], }), router: { location: { query: { transaction: transaction || '/', project: project?.id, ...query, }, }, }, }); act(() => ProjectsStore.loadInitialData(data.organization.projects)); return data; } function WrappedComponent({ location, organization, }: { location: Location; organization: TOrganization; }) { 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 () { beforeEach(function () { // 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/events-has-measurements/', body: {measurements: true}, }); 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: Record = {}; const webVitals = VITAL_GROUPS.reduce( (vs, group) => vs.concat(group.vitals), [] ); for (const measurement of webVitals) { const data: HistogramData[] = []; 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(() => { jest.restoreAllMocks(); }); it('render no access without feature', function () { const {organization, router, routerContext} = initialize({ features: [], }); render(, { context: routerContext, }); expect(screen.getByText("You don't have access to this feature")).toBeInTheDocument(); }); it('renders the basic UI components', function () { const {organization, router, routerContext} = initialize({ transaction: '/organizations/:orgId/', }); render(, { context: routerContext, }); expect( screen.getByRole('heading', {name: '/organizations/:orgId/'}) ).toBeInTheDocument(); ['navigation', 'main'].forEach(role => { expect(screen.getByRole(role)).toBeInTheDocument(); }); }); it('renders the correct bread crumbs', function () { const {organization, router, routerContext} = initialize(); render(, { context: routerContext, }); expect(screen.getByRole('navigation')).toHaveTextContent('PerformanceWeb Vitals'); }); describe('renders all vitals cards correctly', function () { const {organization, router, routerContext} = initialize(); beforeEach(() => { render( , {context: routerContext} ); }); it.each(vitals)('Renders %s', function (vital) { expect(screen.getByText(vital.heading)).toBeInTheDocument(); expect(screen.getByText(vital.baseline)).toBeInTheDocument(); }); }); describe('reset view', function () { it('disables button on default view', function () { const {organization, router, routerContext} = initialize(); render( , {context: routerContext} ); expect(screen.getByRole('button', {name: 'Reset View'})).toBeDisabled(); }); it('enables button on left zoom', function () { const {organization, router, routerContext} = initialize({ query: { lcpStart: '20', }, }); render( , {context: routerContext} ); expect(screen.getByRole('button', {name: 'Reset View'})).toBeEnabled(); }); it('enables button on right zoom', function () { const {organization, router, routerContext} = initialize({ query: { fpEnd: '20', }, }); render( , {context: routerContext} ); expect(screen.getByRole('button', {name: 'Reset View'})).toBeEnabled(); }); it('enables button on left and right zoom', function () { const {organization, router, routerContext} = initialize({ query: { fcpStart: '20', fcpEnd: '20', }, }); render( , {context: routerContext} ); expect(screen.getByRole('button', {name: 'Reset View'})).toBeEnabled(); }); it('resets view properly', async function () { const {organization, router, routerContext} = initialize({ query: { fidStart: '20', lcpEnd: '20', }, }); render( , {context: routerContext} ); await userEvent.click(screen.getByRole('button', {name: 'Reset View'})); 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', }, }); render( , {context: routerContext} ); await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-placeholder') ); expect( screen.getByText( 'If this page is looking a little bare, keep in mind not all browsers support these vitals.' ) ).toBeInTheDocument(); }); 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', }, }); render( , {context: routerContext} ); await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-placeholder') ); expect( screen.queryByText( 'If this page is looking a little bare, keep in mind not all browsers support these vitals.' ) ).not.toBeInTheDocument(); }); }); 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', }, }); render(, { context: routerContext, }); await waitForElementToBeRemoved(() => screen.queryAllByTestId('loading-placeholder')); expect( screen.getByText( 'If this page is looking a little bare, keep in mind not all browsers support these vitals.' ) ).toBeInTheDocument(); }); });