import {browserHistory} from 'react-router';
import {Organization} from 'sentry-fixture/organization';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {
act,
render,
screen,
userEvent,
waitFor,
within,
} from 'sentry-test/reactTestingLibrary';
import * as modals from 'sentry/actionCreators/modal';
import ProjectsStore from 'sentry/stores/projectsStore';
import CreateDashboard from 'sentry/views/dashboards/create';
import * as types from 'sentry/views/dashboards/types';
import ViewEditDashboard from 'sentry/views/dashboards/view';
import {OrganizationContext} from 'sentry/views/organizationContext';
describe('Dashboards > Detail', function () {
const organization = Organization({
features: ['global-views', 'dashboards-basic', 'dashboards-edit', 'discover-query'],
});
const projects = [TestStubs.Project()];
describe('prebuilt dashboards', function () {
let initialData;
beforeEach(function () {
act(() => ProjectsStore.loadInitialData(projects));
initialData = initializeOrg({organization});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: [TestStubs.Project()],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
body: [
TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/default-overview/',
body: TestStubs.Dashboard([], {id: 'default-overview', title: 'Default'}),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/visit/',
method: 'POST',
body: [],
statusCode: 200,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/users/',
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/sdk-updates/',
body: [],
});
MockApiClient.addMockResponse({
url: '/prompts-activity/',
body: {},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: {data: []},
});
MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/issues/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [],
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
});
it('assigns unique IDs to all widgets so grid keys are unique', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: {data: []},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/default-overview/',
body: TestStubs.Dashboard(
[
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'Default Widget 1',
interval: '1d',
}
),
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:transaction',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'Default Widget 2',
interval: '1d',
}
),
],
{id: 'default-overview', title: 'Default'}
),
});
initialData = initializeOrg({
organization: Organization({
features: ['global-views', 'dashboards-basic', 'discover-query'],
projects: [TestStubs.Project()],
}),
});
render(
{null}
,
{context: initialData.routerContext}
);
expect(await screen.findByText('Default Widget 1')).toBeInTheDocument();
expect(screen.getByText('Default Widget 2')).toBeInTheDocument();
});
it('opens the widget viewer modal in a prebuilt dashboard using the widget id specified in the url', async () => {
const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await waitFor(() => {
expect(openWidgetViewerModal).toHaveBeenCalledWith(
expect.objectContaining({
organization: initialData.organization,
widget: expect.objectContaining({
displayType: 'line',
interval: '5m',
queries: [
{
aggregates: ['count()'],
columns: [],
conditions: '!event.type:transaction',
fields: ['count()'],
name: 'Events',
orderby: 'count()',
},
],
title: 'Events',
widgetType: 'discover',
}),
onClose: expect.anything(),
})
);
});
});
});
describe('custom dashboards', function () {
let initialData, widgets, mockVisit, mockPut;
beforeEach(function () {
window.confirm = jest.fn();
initialData = initializeOrg({
organization,
router: {
location: TestStubs.location(),
},
});
widgets = [
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
columns: [],
aggregates: ['count()'],
},
],
{
title: 'Errors',
interval: '1d',
widgetType: 'discover',
id: '1',
}
),
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:transaction',
fields: ['count()'],
columns: [],
aggregates: ['count()'],
},
],
{
title: 'Transactions',
interval: '1d',
widgetType: 'discover',
id: '2',
}
),
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:transaction transaction:/api/cats',
fields: ['p50()'],
columns: [],
aggregates: ['p50()'],
},
],
{
title: 'p50 of /api/cats',
interval: '1d',
id: '3',
}
),
];
mockVisit = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/visit/',
method: 'POST',
body: [],
statusCode: 200,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: [TestStubs.Project()],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
body: [
TestStubs.Dashboard([], {
id: 'default-overview',
title: 'Default',
widgetDisplay: ['area'],
}),
TestStubs.Dashboard([], {
id: '1',
title: 'Custom Errors',
widgetDisplay: ['area'],
}),
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
title: 'Custom Errors',
filters: {},
}),
});
mockPut = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
method: 'PUT',
body: TestStubs.Dashboard(widgets, {id: '1', title: 'Custom Errors'}),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: {data: []},
});
MockApiClient.addMockResponse({
method: 'POST',
url: '/organizations/org-slug/dashboards/widgets/',
body: [],
});
MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/recent-searches/',
body: [],
});
MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/issues/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/users/',
method: 'GET',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/sdk-updates/',
body: [],
});
MockApiClient.addMockResponse({
url: '/prompts-activity/',
body: {},
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
jest.clearAllMocks();
});
it('can remove widgets', async function () {
const updateMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
method: 'PUT',
body: TestStubs.Dashboard([widgets[0]], {id: '1', title: 'Custom Errors'}),
});
render(
{null}
,
{context: initialData.routerContext}
);
await waitFor(() => expect(mockVisit).toHaveBeenCalledTimes(1));
// Enter edit mode.
await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
// Remove the second and third widgets
await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
await userEvent.click(screen.getAllByRole('button', {name: 'Delete Widget'})[1]);
// Save changes
await userEvent.click(screen.getByRole('button', {name: 'Save and Finish'}));
expect(updateMock).toHaveBeenCalled();
expect(updateMock).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/1/',
expect.objectContaining({
data: expect.objectContaining({
title: 'Custom Errors',
widgets: [expect.objectContaining(widgets[0])],
}),
})
);
// Visit should not be called again on dashboard update
expect(mockVisit).toHaveBeenCalledTimes(1);
});
it('appends dashboard-level filters to series request', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
title: 'Custom Errors',
filters: {release: ['abc@1.2.0']},
}),
});
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: [],
});
render(
{null}
,
{context: initialData.routerContext}
);
await waitFor(() =>
expect(mock).toHaveBeenLastCalledWith(
'/organizations/org-slug/events-stats/',
expect.objectContaining({
query: expect.objectContaining({
query: 'event.type:transaction transaction:/api/cats release:abc@1.2.0 ',
}),
})
)
);
});
it('shows add widget option', async function () {
render(
{null}
,
{context: initialData.routerContext}
);
// Enter edit mode.
await userEvent.click(screen.getByRole('button', {name: 'Edit Dashboard'}));
expect(await screen.findByRole('button', {name: 'Add widget'})).toBeInTheDocument();
});
it('shows top level release filter', async function () {
const mockReleases = MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [TestStubs.Release()],
});
initialData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
projects: [TestStubs.Project()],
}),
});
render(
{null}
,
{context: initialData.routerContext}
);
expect(await screen.findByText('All Releases')).toBeInTheDocument();
expect(mockReleases).toHaveBeenCalledTimes(1);
});
it('hides add widget option', async function () {
// @ts-expect-error this is assigning to readonly property...
types.MAX_WIDGETS = 1;
render(
{null}
,
{context: initialData.routerContext}
);
// Enter edit mode.
await userEvent.click(await screen.findByRole('button', {name: 'Edit Dashboard'}));
expect(screen.queryByRole('button', {name: 'Add widget'})).not.toBeInTheDocument();
});
it('renders successfully if more widgets than stored layouts', async function () {
// A case where someone has async added widgets to a dashboard
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(
[
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'First Widget',
interval: '1d',
id: '1',
layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
}
),
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'Second Widget',
interval: '1d',
id: '2',
}
),
],
{id: '1', title: 'Custom Errors'}
),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
expect(await screen.findByText('First Widget')).toBeInTheDocument();
expect(await screen.findByText('Second Widget')).toBeInTheDocument();
});
it('does not trigger request if layout not updated', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(
[
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'First Widget',
interval: '1d',
id: '1',
layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
}
),
],
{id: '1', title: 'Custom Errors'}
),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await userEvent.click(await screen.findByText('Edit Dashboard'));
await userEvent.click(await screen.findByText('Save and Finish'));
expect(screen.getByText('Edit Dashboard')).toBeInTheDocument();
expect(mockPut).not.toHaveBeenCalled();
});
it('renders the custom resize handler for a widget', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(
[
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'First Widget',
interval: '1d',
id: '1',
layout: {i: 'grid-item-1', x: 0, y: 0, w: 2, h: 6},
}
),
],
{id: '1', title: 'Custom Errors'}
),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await userEvent.click(await screen.findByText('Edit Dashboard'));
const widget = screen
.getByText('First Widget')
.closest('.react-grid-item') as HTMLElement;
const resizeHandle = within(widget).getByTestId('custom-resize-handle');
expect(resizeHandle).toBeVisible();
});
it('does not trigger an alert when the widgets have no layout and user cancels without changes', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(
[
TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
},
],
{
title: 'First Widget',
interval: '1d',
id: '1',
layout: null,
}
),
],
{id: '1', title: 'Custom Errors'}
),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await userEvent.click(await screen.findByText('Edit Dashboard'));
await userEvent.click(await screen.findByText('Cancel'));
expect(window.confirm).not.toHaveBeenCalled();
});
it('opens the widget viewer modal using the widget id specified in the url', async () => {
const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
const widget = TestStubs.Widget(
[
{
name: '',
conditions: 'event.type:error',
fields: ['count()'],
aggregates: ['count()'],
columns: [],
orderby: '',
},
],
{
title: 'First Widget',
interval: '1d',
id: '1',
layout: null,
}
);
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard([widget], {id: '1', title: 'Custom Errors'}),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await waitFor(() => {
expect(openWidgetViewerModal).toHaveBeenCalledWith(
expect.objectContaining({
organization: initialData.organization,
widget,
onClose: expect.anything(),
})
);
});
});
it('redirects user to dashboard url if widget is not found', async () => {
const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard([], {id: '1', title: 'Custom Errors'}),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
expect(await screen.findByText('All Releases')).toBeInTheDocument();
expect(openWidgetViewerModal).not.toHaveBeenCalled();
expect(initialData.router.replace).toHaveBeenCalledWith(
expect.objectContaining({
pathname: '/organizations/org-slug/dashboard/1/',
query: {},
})
);
});
it('saves a new dashboard with the page filters', async () => {
const mockPOST = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
method: 'POST',
body: [],
});
render(
{null}
,
{
context: initialData.routerContext,
organization: initialData.organization,
}
);
await userEvent.click(await screen.findByText('Save and Finish'));
expect(mockPOST).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/',
expect.objectContaining({
data: expect.objectContaining({
projects: [2],
environment: ['alpha', 'beta'],
period: '7d',
}),
})
);
});
it('saves a template with the page filters', async () => {
const mockPOST = MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/',
method: 'POST',
body: [],
});
render(
{null}
,
{
context: initialData.routerContext,
organization: initialData.organization,
}
);
await userEvent.click(await screen.findByText('Add Dashboard'));
expect(mockPOST).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/',
expect.objectContaining({
data: expect.objectContaining({
projects: [2],
environment: ['alpha', 'beta'],
period: '7d',
}),
})
);
});
it('does not render save and cancel buttons on templates', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
render(
{null}
,
{
context: initialData.routerContext,
organization: initialData.organization,
}
);
await userEvent.click(await screen.findByText('24H'));
await userEvent.click(screen.getByText('Last 7 days'));
await screen.findByText('7D');
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
expect(screen.queryByText('Save')).not.toBeInTheDocument();
});
it('opens the widget viewer with saved dashboard filters', async () => {
const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
filters: {release: ['sentry-android-shop@1.2.0']},
}),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await waitFor(() => {
expect(openWidgetViewerModal).toHaveBeenCalledWith(
expect.objectContaining({
dashboardFilters: {release: ['sentry-android-shop@1.2.0']},
})
);
});
});
it('opens the widget viewer with unsaved dashboard filters', async () => {
const openWidgetViewerModal = jest.spyOn(modals, 'openWidgetViewerModal');
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
filters: {release: ['sentry-android-shop@1.2.0']},
}),
});
render(
{null}
,
{context: initialData.routerContext, organization: initialData.organization}
);
await waitFor(() => {
expect(openWidgetViewerModal).toHaveBeenCalledWith(
expect.objectContaining({
dashboardFilters: {release: ['unsaved-release-filter@1.2.0']},
})
);
});
});
it('can save dashboard filters in existing dashboard', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
statsPeriod: '7d',
release: ['sentry-android-shop@1.2.0'],
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await userEvent.click(await screen.findByText('Save'));
expect(mockPut).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/1/',
expect.objectContaining({
data: expect.objectContaining({
period: '7d',
filters: {release: ['sentry-android-shop@1.2.0']},
}),
})
);
});
it('can clear dashboard filters in compact select', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
title: 'Custom Errors',
filters: {release: ['sentry-android-shop@1.2.0']},
}),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
statsPeriod: '7d',
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await screen.findByText('7D');
await userEvent.click(await screen.findByText('sentry-android-shop@1.2.0'));
await userEvent.click(screen.getByText('Clear'));
screen.getByText('All Releases');
await userEvent.click(document.body);
expect(browserHistory.push).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({
release: '',
}),
})
);
});
it('can save absolute time range in existing dashboard', async () => {
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
start: '2022-07-14T07:00:00',
end: '2022-07-19T23:59:59',
utc: 'true',
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await userEvent.click(await screen.findByText('Save'));
expect(mockPut).toHaveBeenCalledWith(
'/organizations/org-slug/dashboards/1/',
expect.objectContaining({
data: expect.objectContaining({
start: '2022-07-14T07:00:00.000',
end: '2022-07-19T23:59:59.000',
utc: true,
}),
})
);
});
it('can clear dashboard filters in existing dashboard', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
statsPeriod: '7d',
environment: ['alpha', 'beta'],
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await screen.findByText('7D');
await userEvent.click(await screen.findByText('All Releases'));
await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
await userEvent.keyboard('{Escape}');
await userEvent.click(screen.getByText('Cancel'));
screen.getByText('All Releases');
expect(browserHistory.replace).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({
project: undefined,
statsPeriod: undefined,
environment: undefined,
}),
})
);
});
it('disables the Edit Dashboard button when there are unsaved filters', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-basic',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
statsPeriod: '7d',
environment: ['alpha', 'beta'],
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
expect(await screen.findByText('Save')).toBeInTheDocument();
expect(screen.getByText('Cancel')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Edit Dashboard'})).toBeDisabled();
});
it('ignores the order of selection of page filters to render unsaved filters', async () => {
const testProjects = [
TestStubs.Project({id: '1', name: 'first', environments: ['alpha', 'beta']}),
TestStubs.Project({id: '2', name: 'second', environments: ['alpha', 'beta']}),
];
act(() => ProjectsStore.loadInitialData(testProjects));
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: testProjects,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
title: 'Custom Errors',
filters: {},
environment: ['alpha', 'beta'],
}),
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
environment: ['beta', 'alpha'], // Reversed order from saved dashboard
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await waitFor(() => expect(screen.queryAllByText('Loading\u2026')).toEqual([]));
await screen.findByText(/beta, alpha/);
// Save and Cancel should not appear because alpha, beta is the same as beta, alpha
expect(screen.queryByText('Save')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
});
it('uses releases from the URL query params', async function () {
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
release: ['not-selected-1'],
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await screen.findByText(/not-selected-1/);
screen.getByText('Save');
screen.getByText('Cancel');
});
it('resets release in URL params', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/dashboards/1/',
body: TestStubs.Dashboard(widgets, {
id: '1',
title: 'Custom Errors',
filters: {
release: ['abc'],
},
}),
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: {
...TestStubs.location(),
query: {
release: ['not-selected-1'],
},
},
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await screen.findByText(/not-selected-1/);
await userEvent.click(screen.getByText('Cancel'));
// release isn't used in the redirect
expect(browserHistory.replace).toHaveBeenCalledWith(
expect.objectContaining({
query: {
end: undefined,
environment: undefined,
project: undefined,
start: undefined,
statsPeriod: undefined,
utc: undefined,
},
})
);
});
it('reflects selections in the release filter in the query params', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-query',
],
}),
router: {
location: TestStubs.location(),
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await userEvent.click(await screen.findByText('All Releases'));
await userEvent.click(screen.getByText('sentry-android-shop@1.2.0'));
await userEvent.click(document.body);
expect(browserHistory.push).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({
release: ['sentry-android-shop@1.2.0'],
}),
})
);
});
it('persists release selections made during search requests that do not appear in default query', async function () {
// Default response
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
shortVersion: 'sentry-android-shop@1.2.0',
version: 'sentry-android-shop@1.2.0',
}),
],
});
// Mocked search results
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
TestStubs.Release({
id: '9',
shortVersion: 'search-result',
version: 'search-result',
}),
],
match: [MockApiClient.matchData({query: 's'})],
});
const testData = initializeOrg({
organization: Organization({
features: [
'global-views',
'dashboards-basic',
'dashboards-edit',
'discover-basic',
'discover-query',
],
}),
router: {
location: TestStubs.location(),
},
});
render(
{null}
,
{context: testData.routerContext, organization: testData.organization}
);
await userEvent.click(await screen.findByText('All Releases'));
await userEvent.type(screen.getByPlaceholderText('Search\u2026'), 's');
await userEvent.click(await screen.findByRole('option', {name: 'search-result'}));
// Validate that after search is cleared, search result still appears
expect(await screen.findByText('Latest Release(s)')).toBeInTheDocument();
expect(screen.getByRole('option', {name: 'search-result'})).toBeInTheDocument();
});
});
});