import {browserHistory} from 'react-router';
import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
import {triggerPress} from 'sentry-test/utils';
import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence';
import ProjectsStore from 'sentry/stores/projectsStore';
import EventView from 'sentry/utils/discover/eventView';
import Results from 'sentry/views/eventsV2/results';
import {OrganizationContext} from 'sentry/views/organizationContext';
import {TRANSACTION_VIEWS} from './data';
const FIELDS = [
{
field: 'title',
},
{
field: 'timestamp',
},
{
field: 'user',
},
{
field: 'count()',
},
];
const generateFields = () => ({
field: FIELDS.map(i => i.field),
});
describe('Results', function () {
enforceActOnUseLegacyStoreHook();
const eventTitle = 'Oh no something bad';
let eventsResultsMock, eventsv2ResultsMock, mockSaved, eventsStatsMock, mockVisit;
const mountWithThemeAndOrg = (component, opts, organization) =>
mountWithTheme(component, {
...opts,
wrappingComponent: ({children}) => (
{children}
),
});
beforeEach(function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects-count/',
body: {myProjects: 10, allProjects: 300},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
body: [],
});
eventsStatsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: {data: [[123, []]]},
});
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/releases/stats/',
body: [],
});
const eventsV2ResultsMockBody = {
meta: {
id: 'string',
title: 'string',
'project.name': 'string',
timestamp: 'date',
'user.id': 'string',
},
data: [
{
id: 'deadbeef',
'user.id': 'alberto leal',
title: eventTitle,
'project.name': 'project-slug',
timestamp: '2019-05-23T22:12:48+00:00',
},
],
};
const eventsResultsMockBody = {
meta: {
fields: {
id: 'string',
title: 'string',
'project.name': 'string',
timestamp: 'date',
'user.id': 'string',
},
},
data: [
{
id: 'deadbeef',
'user.id': 'alberto leal',
title: eventTitle,
'project.name': 'project-slug',
timestamp: '2019-05-23T22:12:48+00:00',
},
],
};
eventsv2ResultsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/eventsv2/',
body: eventsV2ResultsMockBody,
});
eventsResultsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: eventsResultsMockBody,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-meta/',
body: {
count: 2,
},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/project-slug:deadbeef/',
method: 'GET',
body: {
id: '1234',
size: 1200,
eventID: 'deadbeef',
title: 'Oh no something bad',
message: 'It was not good',
dateCreated: '2019-05-23T22:12:48+00:00',
entries: [
{
type: 'message',
message: 'bad stuff',
data: {},
},
],
tags: [{key: 'browser', value: 'Firefox'}],
},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-facets/',
body: [
{
key: 'release',
topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
},
{
key: 'environment',
topValues: [{count: 2, value: 'dev', name: 'dev'}],
},
{
key: 'foo',
topValues: [{count: 1, value: 'bar', name: 'bar'}],
},
],
});
mockVisit = MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/saved/1/visit/',
method: 'POST',
body: [],
statusCode: 200,
});
mockSaved = MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/saved/1/',
method: 'GET',
statusCode: 200,
body: {
id: '1',
name: 'new',
projects: [],
version: 2,
expired: false,
dateCreated: '2021-04-08T17:53:25.195782Z',
dateUpdated: '2021-04-09T12:13:18.567264Z',
createdBy: {
id: '2',
},
environment: [],
fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
widths: ['-1', '-1', '-1', '-1', '-1'],
range: '24h',
orderby: '-user.display',
},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/homepage/',
method: 'GET',
statusCode: 200,
body: {
id: '2',
name: '',
projects: [],
version: 2,
expired: false,
dateCreated: '2021-04-08T17:53:25.195782Z',
dateUpdated: '2021-04-09T12:13:18.567264Z',
createdBy: {
id: '2',
},
environment: [],
fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
widths: ['-1', '-1', '-1', '-1', '-1'],
range: '24h',
orderby: '-user.display',
},
});
});
afterEach(function () {
jest.clearAllMocks();
MockApiClient.clearMockResponses();
act(() => ProjectsStore.reset());
});
describe('EventsV2', function () {
const features = ['discover-basic'];
it('loads data when moving from an invalid to valid EventView', async function () {
const organization = TestStubs.Organization({
features,
});
// Start off with an invalid view (empty is invalid)
const initialData = initializeOrg({
organization,
router: {
location: {query: {query: 'tag:value'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
// No request as eventview was invalid.
expect(eventsv2ResultsMock).not.toHaveBeenCalled();
// Should redirect and retain the old query value..
expect(browserHistory.replace).toHaveBeenCalledWith(
expect.objectContaining({
pathname: '/organizations/org-slug/discover/results/',
query: expect.objectContaining({
query: 'tag:value',
}),
})
);
// Update location simulating a redirect.
wrapper.setProps({location: {query: {...generateFields()}}});
wrapper.update();
// Should load events once
expect(eventsv2ResultsMock).toHaveBeenCalled();
});
it('pagination cursor should be cleared when making a search', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
...generateFields(),
cursor: '0%3A50%3A0',
},
},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
// ensure cursor query string is initially present in the location
expect(initialData.router.location).toEqual({
query: {
...generateFields(),
cursor: '0%3A50%3A0',
},
});
// perform a search
const search = wrapper.find('#smart-search-input').first();
search.simulate('change', {target: {value: 'geo:canada'}}).simulate('submit', {
preventDefault() {},
});
await tick();
// should only be called with saved queries
expect(mockVisit).not.toHaveBeenCalled();
// cursor query string should be omitted from the query string
expect(initialData.router.push).toHaveBeenCalledWith({
pathname: undefined,
query: {
...generateFields(),
query: 'geo:canada',
statsPeriod: '14d',
},
});
wrapper.unmount();
});
it('renders a y-axis selector', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// y-axis selector is last.
const selector = wrapper.find('OptionSelector').last();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Click one of the options.
wrapper.find('Option').first().simulate('click');
await tick();
wrapper.update();
const eventsRequest = wrapper.find('EventsChart');
expect(eventsRequest.props().yAxis).toEqual(['count()']);
wrapper.unmount();
});
it('renders a display selector', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
await tick();
wrapper.update();
// display selector is first.
const selector = wrapper.find('OptionSelector').first();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Click the 'default' option.
wrapper.find('Option').first().simulate('click');
await tick();
wrapper.update();
const eventsRequest = wrapper.find('EventsChart').props();
expect(eventsRequest.disableReleases).toEqual(false);
expect(eventsRequest.disablePrevious).toEqual(true);
wrapper.unmount();
});
it('excludes top5 options when plan does not include discover-query', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'previous'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// display selector is first.
const selector = wrapper.find('OptionSelector').first();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Make sure the top5 option isn't present
const options = wrapper
.find('Option [data-test-id]')
.map(item => item.prop('data-test-id'));
expect(options).not.toContain('top5');
expect(options).not.toContain('dailytop5');
expect(options).toContain('default');
wrapper.unmount();
});
it('needs confirmation on long queries', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(true);
wrapper.unmount();
});
it('needs confirmation on long query with explicit projects', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
...generateFields(),
statsPeriod: '60d',
project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(true);
wrapper.unmount();
});
it('does not need confirmation on short queries', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(false);
wrapper.unmount();
});
it('does not need confirmation with to few projects', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {...generateFields(), statsPeriod: '90d', project: [1, 2, 3, 4]},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(false);
wrapper.unmount();
});
it('retrieves saved query', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {id: '1', statsPeriod: '24h'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const savedQuery = wrapper.find('SavedQueryAPI').state('savedQuery');
expect(savedQuery.name).toEqual('new');
expect(savedQuery.id).toEqual('1');
expect(savedQuery.fields).toEqual([
'title',
'event.type',
'project',
'user.display',
'timestamp',
]);
expect(savedQuery.projects).toEqual([]);
expect(savedQuery.range).toEqual('24h');
expect(mockSaved).toHaveBeenCalled();
expect(mockVisit).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('creates event view from saved query', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {id: '1', statsPeriod: '24h'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const eventView = wrapper.find('Results').state('eventView');
expect(eventView.name).toEqual('new');
expect(eventView.id).toEqual('1');
expect(eventView.fields.length).toEqual(5);
expect(eventView.project).toEqual([]);
expect(eventView.statsPeriod).toEqual('24h');
expect(eventView.sorts).toEqual([{field: 'user.display', kind: 'desc'}]);
wrapper.unmount();
});
it('overrides saved query params with location query params', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
id: '1',
statsPeriod: '7d',
project: [2],
environment: ['production'],
},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const eventView = wrapper.find('Results').state('eventView');
expect(eventView.name).toEqual('new');
expect(eventView.id).toEqual('1');
expect(eventView.fields.length).toEqual(5);
expect(eventView.project).toEqual([2]);
expect(eventView.statsPeriod).toEqual('7d');
expect(eventView.environment).toEqual(['production']);
expect(mockVisit).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('updates chart whenever yAxis parameter changes', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), yAxis: 'count_unique(user)'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count_unique(user)'],
}),
})
);
wrapper.unmount();
});
it('updates chart whenever display parameter changes', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), display: 'previous', yAxis: 'count()'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '28d',
yAxis: ['count()'],
}),
})
);
wrapper.unmount();
});
it('updates chart whenever display and yAxis parameters change', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), display: 'previous', yAxis: 'count_unique(user)'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '28d',
yAxis: ['count_unique(user)'],
}),
})
);
wrapper.unmount();
});
});
describe('Events', function () {
const features = ['discover-basic', 'discover-frontend-use-events-endpoint'];
it('loads data when moving from an invalid to valid EventView', async function () {
const organization = TestStubs.Organization({
features,
});
// Start off with an invalid view (empty is invalid)
const initialData = initializeOrg({
organization,
router: {
location: {query: {query: 'tag:value'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
// No request as eventview was invalid.
expect(eventsResultsMock).not.toHaveBeenCalled();
// Should redirect and retain the old query value..
expect(browserHistory.replace).toHaveBeenCalledWith(
expect.objectContaining({
pathname: '/organizations/org-slug/discover/results/',
query: expect.objectContaining({
query: 'tag:value',
}),
})
);
// Update location simulating a redirect.
wrapper.setProps({location: {query: {...generateFields()}}});
wrapper.update();
// Should load events once
expect(eventsResultsMock).toHaveBeenCalled();
});
it('pagination cursor should be cleared when making a search', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
...generateFields(),
cursor: '0%3A50%3A0',
},
},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
// ensure cursor query string is initially present in the location
expect(initialData.router.location).toEqual({
query: {
...generateFields(),
cursor: '0%3A50%3A0',
},
});
// perform a search
const search = wrapper.find('#smart-search-input').first();
search.simulate('change', {target: {value: 'geo:canada'}}).simulate('submit', {
preventDefault() {},
});
await tick();
// should only be called with saved queries
expect(mockVisit).not.toHaveBeenCalled();
// cursor query string should be omitted from the query string
expect(initialData.router.push).toHaveBeenCalledWith({
pathname: undefined,
query: {
...generateFields(),
query: 'geo:canada',
statsPeriod: '14d',
},
});
wrapper.unmount();
});
it('renders a y-axis selector', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// y-axis selector is last.
const selector = wrapper.find('OptionSelector').last();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Click one of the options.
wrapper.find('Option').first().simulate('click');
await tick();
wrapper.update();
const eventsRequest = wrapper.find('EventsChart');
expect(eventsRequest.props().yAxis).toEqual(['count()']);
wrapper.unmount();
});
it('renders a display selector', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
await tick();
wrapper.update();
// display selector is first.
const selector = wrapper.find('OptionSelector').first();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Click the 'default' option.
wrapper.find('Option').first().simulate('click');
await tick();
wrapper.update();
const eventsRequest = wrapper.find('EventsChart').props();
expect(eventsRequest.disableReleases).toEqual(false);
expect(eventsRequest.disablePrevious).toEqual(true);
wrapper.unmount();
});
it('excludes top5 options when plan does not include discover-query', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'previous'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// display selector is first.
const selector = wrapper.find('OptionSelector').first();
// Open the selector
act(() => {
triggerPress(selector.find('button[aria-haspopup="listbox"]'));
});
await tick();
wrapper.update();
// Make sure the top5 option isn't present
const options = wrapper
.find('Option [data-test-id]')
.map(item => item.prop('data-test-id'));
expect(options).not.toContain('top5');
expect(options).not.toContain('dailytop5');
expect(options).toContain('default');
wrapper.unmount();
});
it('needs confirmation on long queries', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(true);
wrapper.unmount();
});
it('needs confirmation on long query with explicit projects', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
...generateFields(),
statsPeriod: '60d',
project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(true);
wrapper.unmount();
});
it('does not need confirmation on short queries', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(false);
wrapper.unmount();
});
it('does not need confirmation with to few projects', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {...generateFields(), statsPeriod: '90d', project: [1, 2, 3, 4]},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const results = wrapper.find('Results');
expect(results.state('needConfirmation')).toEqual(false);
wrapper.unmount();
});
it('retrieves saved query', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {id: '1', statsPeriod: '24h'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const savedQuery = wrapper.find('SavedQueryAPI').state('savedQuery');
expect(savedQuery.name).toEqual('new');
expect(savedQuery.id).toEqual('1');
expect(savedQuery.fields).toEqual([
'title',
'event.type',
'project',
'user.display',
'timestamp',
]);
expect(savedQuery.projects).toEqual([]);
expect(savedQuery.range).toEqual('24h');
expect(mockSaved).toHaveBeenCalled();
expect(mockVisit).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('creates event view from saved query', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {id: '1', statsPeriod: '24h'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const eventView = wrapper.find('Results').state('eventView');
expect(eventView.name).toEqual('new');
expect(eventView.id).toEqual('1');
expect(eventView.fields.length).toEqual(5);
expect(eventView.project).toEqual([]);
expect(eventView.statsPeriod).toEqual('24h');
expect(eventView.sorts).toEqual([{field: 'user.display', kind: 'desc'}]);
wrapper.unmount();
});
it('overrides saved query params with location query params', async function () {
const organization = TestStubs.Organization({
features,
slug: 'org-slug',
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
id: '1',
statsPeriod: '7d',
project: [2],
environment: ['production'],
},
},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
const eventView = wrapper.find('Results').state('eventView');
expect(eventView.name).toEqual('new');
expect(eventView.id).toEqual('1');
expect(eventView.fields.length).toEqual(5);
expect(eventView.project).toEqual([2]);
expect(eventView.statsPeriod).toEqual('7d');
expect(eventView.environment).toEqual(['production']);
expect(mockVisit).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
it('updates chart whenever yAxis parameter changes', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), yAxis: 'count_unique(user)'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count_unique(user)'],
}),
})
);
wrapper.unmount();
});
it('updates chart whenever display parameter changes', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), display: 'previous', yAxis: 'count()'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '28d',
yAxis: ['count()'],
}),
})
);
wrapper.unmount();
});
it('updates chart whenever display and yAxis parameters change', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
// Should load events once
expect(eventsStatsMock).toHaveBeenCalledTimes(1);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '14d',
yAxis: ['count()'],
}),
})
);
// Update location simulating a browser back button action
wrapper.setProps({
location: {
query: {...generateFields(), display: 'previous', yAxis: 'count_unique(user)'},
},
});
await tick();
wrapper.update();
// Should load events again
expect(eventsStatsMock).toHaveBeenCalledTimes(2);
expect(eventsStatsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
statsPeriod: '28d',
yAxis: ['count_unique(user)'],
}),
})
);
wrapper.unmount();
});
it('appends tag value to existing query when clicked', async function () {
const organization = TestStubs.Organization({
features,
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
},
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
act(() => ProjectsStore.loadInitialData([TestStubs.Project()]));
await tick();
wrapper.update();
wrapper.find('[data-test-id="toggle-show-tags"]').first().simulate('click');
await tick();
wrapper.update();
// since environment collides with the environment field, it is wrapped with `tags[...]`
const envSegment = wrapper.find(
'[data-test-id="tag-environment-segment-dev"] Segment'
);
const envTarget = envSegment.props().to;
expect(envTarget.query.query).toEqual('tags[environment]:dev');
const fooSegment = wrapper.find('[data-test-id="tag-foo-segment-bar"] Segment');
const fooTarget = fooSegment.props().to;
expect(fooTarget.query.query).toEqual('foo:bar');
});
it('respects pinned filters for prebuilt queries', async function () {
const organization = TestStubs.Organization({
features: [...features, 'global-views'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
},
});
jest.spyOn(PageFilterPersistence, 'getPageFilterStorage').mockReturnValue({
state: {
project: [1],
environment: [],
start: null,
end: null,
period: '14d',
utc: null,
},
pinnedFilters: new Set(['projects']),
});
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
act(() =>
ProjectsStore.loadInitialData([
TestStubs.Project({id: 1, slug: 'Pinned Project'}),
])
);
await tick();
wrapper.update();
const projectPageFilter = wrapper
.find('[data-test-id="page-filter-project-selector"]')
.first();
expect(projectPageFilter.text()).toEqual('Pinned Project');
});
});
it('renders metric fallback alert', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {fromMetric: true}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
expect(wrapper.find('Alert').find('Message').text()).toEqual(
"You've navigated to this page from a performance metric widget generated from processed events. The results here only show indexed events."
);
});
it('renders unparameterized data banner', async function () {
const organization = TestStubs.Organization({
features: ['discover-basic'],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {showUnparameterizedBanner: true}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const wrapper = mountWithThemeAndOrg(
,
initialData.routerContext,
organization
);
await tick();
wrapper.update();
expect(wrapper.find('Alert').find('Message').text()).toEqual(
'These are unparameterized transactions. To better organize your transactions, set transaction names manually.'
);
});
it('updates the homepage query with up to date eventView when Use as Discover Home is clicked', async () => {
const mockHomepageUpdate = MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/homepage/',
method: 'PUT',
statusCode: 200,
});
const organization = TestStubs.Organization({
features: [
'discover-basic',
'discover-query',
'discover-query-builder-as-landing-page',
],
});
const initialData = initializeOrg({
organization,
router: {
// These fields take priority and should be sent in the request
location: {query: {field: ['title', 'user'], id: '1'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
render(
,
{context: initialData.routerContext, organization}
);
await waitFor(() =>
expect(screen.getByRole('button', {name: /use as discover home/i})).toBeEnabled()
);
userEvent.click(screen.getByText('Use as Discover Home'));
expect(mockHomepageUpdate).toHaveBeenCalledWith(
'/organizations/org-slug/discover/homepage/',
expect.objectContaining({
data: expect.objectContaining({
fields: ['title', 'user'],
}),
})
);
});
it('Changes the Use as Discover button to a reset button for saved query', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/homepage/',
method: 'PUT',
statusCode: 200,
body: {
id: '2',
name: '',
projects: [],
version: 2,
expired: false,
dateCreated: '2021-04-08T17:53:25.195782Z',
dateUpdated: '2021-04-09T12:13:18.567264Z',
createdBy: {
id: '2',
},
environment: [],
fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
widths: ['-1', '-1', '-1', '-1', '-1'],
range: '14d',
orderby: '-user.display',
},
});
const organization = TestStubs.Organization({
features: [
'discover-basic',
'discover-query',
'discover-query-builder-as-landing-page',
],
});
const initialData = initializeOrg({
organization,
router: {
location: {query: {id: '1'}},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const {rerender} = render(
,
{context: initialData.routerContext, organization}
);
await waitFor(() =>
expect(screen.getByRole('button', {name: /use as discover home/i})).toBeEnabled()
);
userEvent.click(screen.getByText('Use as Discover Home'));
expect(await screen.findByText('Reset Discover Home')).toBeInTheDocument();
userEvent.click(screen.getByText('Total Period'));
userEvent.click(screen.getByText('Previous Period'));
const rerenderData = initializeOrg({
organization,
router: {
location: {query: {...initialData.router.location.query, display: 'previous'}},
},
});
rerender(
);
screen.getByText('Previous Period');
expect(await screen.findByText('Use as Discover Home')).toBeInTheDocument();
});
it('Changes the Use as Discover button to a reset button for prebuilt query', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/discover/homepage/',
method: 'PUT',
statusCode: 200,
body: {...TRANSACTION_VIEWS[0], name: ''},
});
const organization = TestStubs.Organization({
features: [
'discover-basic',
'discover-query',
'discover-query-builder-as-landing-page',
],
});
const initialData = initializeOrg({
organization,
router: {
location: {
...TestStubs.location(),
query: {
...EventView.fromNewQueryWithLocation(
TRANSACTION_VIEWS[0],
TestStubs.location()
).generateQueryStringObject(),
},
},
},
});
ProjectsStore.loadInitialData([TestStubs.Project()]);
const {rerender} = render(
,
{context: initialData.routerContext, organization}
);
await screen.findAllByText(TRANSACTION_VIEWS[0].name);
userEvent.click(screen.getByText('Use as Discover Home'));
expect(await screen.findByText('Reset Discover Home')).toBeInTheDocument();
userEvent.click(screen.getByText('Total Period'));
userEvent.click(screen.getByText('Previous Period'));
const rerenderData = initializeOrg({
organization,
router: {
location: {query: {...initialData.router.location.query, display: 'previous'}},
},
});
rerender(
);
screen.getByText('Previous Period');
expect(await screen.findByText('Use as Discover Home')).toBeInTheDocument();
});
});