import {mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import MemberListStore from 'sentry/stores/memberListStore'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import Dashboard from 'sentry/views/dashboardsV2/dashboard'; import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboardsV2/types'; import {OrganizationContext} from '../organizationContext'; describe('Dashboards > Dashboard', () => { const organization = TestStubs.Organization({ features: ['dashboards-basic', 'dashboards-edit', 'dashboard-grid-layout'], }); const organizationWithFlag = TestStubs.Organization({ features: ['dashboards-basic', 'dashboards-edit', 'dashboard-grid-layout'], }); const mockDashboard = { dateCreated: '2021-08-10T21:20:46.798237Z', id: '1', title: 'Test Dashboard', widgets: [], projects: [], filters: {}, }; const newWidget: Widget = { id: '1', title: 'Test Discover Widget', displayType: DisplayType.LINE, widgetType: WidgetType.DISCOVER, interval: '5m', queries: [ { name: '', conditions: '', fields: ['count()'], aggregates: ['count()'], columns: [], orderby: '', }, ], }; const issueWidget: Widget = { id: '2', title: 'Test Issue Widget', displayType: DisplayType.TABLE, widgetType: WidgetType.ISSUE, interval: '5m', queries: [ { name: '', conditions: '', fields: ['title', 'assignee'], aggregates: [], columns: ['title', 'assignee'], orderby: '', }, ], }; let initialData, tagsMock; beforeEach(() => { initialData = initializeOrg({organization, router: {}, project: 1, projects: []}); MockApiClient.addMockResponse({ url: `/organizations/org-slug/dashboards/widgets/`, method: 'POST', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-stats/', method: 'GET', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/', method: 'GET', body: [ { annotations: [], id: '1', title: 'Error: Failed', project: { id: '3', }, owners: [ { type: 'ownershipRule', owner: 'user:2', }, ], }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/users/', method: 'GET', body: [ { user: { id: '2', name: 'test@sentry.io', email: 'test@sentry.io', avatar: { avatarType: 'letter_avatar', avatarUuid: null, }, }, }, ], }); tagsMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/', method: 'GET', body: TestStubs.Tags(), }); }); it('fetches tags', () => { mountWithTheme( undefined} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={() => undefined} router={initialData.router} location={initialData.router.location} widgetLimitReached={false} isEditing={false} />, initialData.routerContext ); expect(tagsMock).toHaveBeenCalled(); }); it('dashboard adds new widget if component is mounted with newWidget prop', async () => { const mockHandleAddCustomWidget = jest.fn(); const mockCallbackToUnsetNewWidget = jest.fn(); const wrapper = mountWithTheme( undefined} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={mockHandleAddCustomWidget} router={initialData.router} location={initialData.router.location} newWidget={newWidget} widgetLimitReached={false} onSetNewWidget={mockCallbackToUnsetNewWidget} />, initialData.routerContext ); await tick(); wrapper.update(); expect(mockHandleAddCustomWidget).toHaveBeenCalled(); expect(mockCallbackToUnsetNewWidget).toHaveBeenCalled(); }); it('dashboard adds new widget if component updated with newWidget prop', async () => { const mockHandleAddCustomWidget = jest.fn(); const mockCallbackToUnsetNewWidget = jest.fn(); const wrapper = mountWithTheme( undefined} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={mockHandleAddCustomWidget} router={initialData.router} location={initialData.router.location} widgetLimitReached={false} onSetNewWidget={mockCallbackToUnsetNewWidget} />, initialData.routerContext ); expect(mockHandleAddCustomWidget).not.toHaveBeenCalled(); expect(mockCallbackToUnsetNewWidget).not.toHaveBeenCalled(); wrapper.setProps({newWidget}); await tick(); wrapper.update(); expect(mockHandleAddCustomWidget).toHaveBeenCalled(); expect(mockCallbackToUnsetNewWidget).toHaveBeenCalled(); }); it('dashboard does not try to add new widget if no newWidget', async () => { const mockHandleAddCustomWidget = jest.fn(); const mockCallbackToUnsetNewWidget = jest.fn(); const wrapper = mountWithTheme( undefined} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={mockHandleAddCustomWidget} router={initialData.router} location={initialData.router.location} widgetLimitReached={false} onSetNewWidget={mockCallbackToUnsetNewWidget} />, initialData.routerContext ); await tick(); wrapper.update(); expect(mockHandleAddCustomWidget).not.toHaveBeenCalled(); expect(mockCallbackToUnsetNewWidget).not.toHaveBeenCalled(); }); describe('Issue Widgets', () => { beforeEach(() => { MemberListStore.init(); }); const mount = (dashboard, mockedOrg = initialData.organization) => { render( undefined} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={() => undefined} router={initialData.router} location={initialData.router.location} widgetLimitReached={false} /> ); }; it('dashboard displays issue widgets if the user has issue widgets feature flag', async () => { const mockDashboardWithIssueWidget = { ...mockDashboard, widgets: [newWidget, issueWidget], }; await act(async () => { mount(mockDashboardWithIssueWidget, organizationWithFlag); await tick(); }); expect(screen.getByText('Test Discover Widget')).toBeInTheDocument(); expect(screen.getByText('Test Issue Widget')).toBeInTheDocument(); }); it('renders suggested assignees', async () => { const mockDashboardWithIssueWidget = { ...mockDashboard, widgets: [{...issueWidget}], }; mount(mockDashboardWithIssueWidget, organizationWithFlag); expect(await screen.findByText('T')).toBeInTheDocument(); userEvent.hover(screen.getByText('T')); expect(await screen.findByText('Suggestion:')).toBeInTheDocument(); expect(screen.getByText('test@sentry.io')).toBeInTheDocument(); expect(screen.getByText('Matching Issue Owners Rule')).toBeInTheDocument(); }); }); describe('Edit mode', () => { let widgets: Widget[]; const mount = ( dashboard, mockedOrg = initialData.organization, mockedRouter = initialData.router, mockedLocation = initialData.router.location ) => { const getDashboardComponent = () => ( { widgets.splice(0, widgets.length, ...newWidgets); }} handleUpdateWidgetList={() => undefined} handleAddCustomWidget={() => undefined} router={mockedRouter} location={mockedLocation} widgetLimitReached={false} /> ); const {rerender} = render(getDashboardComponent()); return {rerender: () => rerender(getDashboardComponent())}; }; beforeEach(() => { widgets = [newWidget]; }); it('displays the copy widget button in edit mode', async () => { const dashboardWithOneWidget = {...mockDashboard, widgets}; await act(async () => { mount(dashboardWithOneWidget); await tick(); }); expect(screen.getByLabelText('Duplicate Widget')).toBeInTheDocument(); }); it('duplicates the widget', async () => { const dashboardWithOneWidget = {...mockDashboard, widgets}; await act(async () => { const {rerender} = mount(dashboardWithOneWidget); userEvent.click(screen.getByLabelText('Duplicate Widget')); rerender(); await tick(); }); expect(screen.getAllByText('Test Discover Widget')).toHaveLength(2); }); it('opens the widget builder when editing with the modal access flag', async function () { const testData = initializeOrg({ ...initializeOrg(), organization: { features: ['dashboards-basic', 'dashboards-edit', 'dashboard-grid-layout'], }, }); const dashboardWithOneWidget = { ...mockDashboard, widgets: [newWidget], }; await act(async () => { mount( dashboardWithOneWidget, testData.organization, testData.router, testData.router.location ); await tick(); userEvent.click(screen.getByLabelText('Edit Widget')); }); expect(testData.router.push).toHaveBeenCalledWith( expect.objectContaining({ pathname: '/organizations/org-slug/dashboard/1/widget/0/edit/', }) ); }); }); });