mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
415 lines
11 KiB
TypeScript
415 lines
11 KiB
TypeScript
import {browserHistory} from 'react-router';
|
|
import type {Location, Query} from 'history';
|
|
import {OrganizationFixture} from 'sentry-fixture/organization';
|
|
import {ProjectFixture} from 'sentry-fixture/project';
|
|
|
|
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 || ProjectFixture();
|
|
query = query || {};
|
|
const data = initializeOrg({
|
|
organization: OrganizationFixture({
|
|
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 (
|
|
<OrganizationContext.Provider value={organization}>
|
|
<TransactionVitals location={location} organization={organization} />
|
|
</OrganizationContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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<string, HistogramData[]> = {};
|
|
const webVitals = VITAL_GROUPS.reduce<string[]>(
|
|
(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: '/organizations/org-slug/prompts-activity/',
|
|
body: {},
|
|
});
|
|
|
|
MockApiClient.addMockResponse({
|
|
url: '/organizations/org-slug/sdk-updates/',
|
|
body: [],
|
|
});
|
|
MockApiClient.addMockResponse({
|
|
url: '/organizations/org-slug/replay-count/',
|
|
body: {},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('render no access without feature', function () {
|
|
const {organization, router, routerContext} = initialize({
|
|
features: [],
|
|
});
|
|
|
|
render(<WrappedComponent organization={organization} location={router.location} />, {
|
|
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(<WrappedComponent organization={organization} location={router.location} />, {
|
|
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(<WrappedComponent organization={organization} location={router.location} />, {
|
|
context: routerContext,
|
|
});
|
|
|
|
expect(screen.getByRole('navigation')).toHaveTextContent('PerformanceWeb Vitals');
|
|
});
|
|
|
|
describe('renders all vitals cards correctly', function () {
|
|
const {organization, router, routerContext} = initialize();
|
|
|
|
beforeEach(() => {
|
|
render(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(
|
|
<WrappedComponent organization={organization} location={router.location} />,
|
|
{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(<WrappedComponent organization={organization} location={router.location} />, {
|
|
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();
|
|
});
|
|
});
|