import {browserHistory} from 'react-router'; import {mountWithTheme} from 'sentry-test/enzyme'; import {mountGlobalModal} from 'sentry-test/modal'; import {act} from 'sentry-test/reactTestingLibrary'; import {selectByValue} from 'sentry-test/select-new'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {removePageFiltersStorage} from 'sentry/components/organizations/pageFilters/persistence'; import ProjectsStore from 'sentry/stores/projectsStore'; import ProjectContext from 'sentry/views/projects/projectContext'; import ProjectGeneralSettings from 'sentry/views/settings/projectGeneralSettings'; jest.mock('sentry/actionCreators/indicator'); jest.mock('sentry/components/organizations/pageFilters/persistence'); describe('projectGeneralSettings', function () { const org = TestStubs.Organization(); const project = TestStubs.ProjectDetails(); const groupingConfigs = TestStubs.GroupingConfigs(); const groupingEnhancements = TestStubs.GroupingEnhancements(); let routerContext; let putMock; let wrapper; let modal; beforeEach(function () { jest.spyOn(window.location, 'assign'); routerContext = TestStubs.routerContext([ { router: TestStubs.router({ params: { projectId: project.slug, orgId: org.slug, }, }), }, ]); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/grouping-configs/', method: 'GET', body: groupingConfigs, }); MockApiClient.addMockResponse({ url: '/grouping-enhancements/', method: 'GET', body: groupingEnhancements, }); MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/`, method: 'GET', body: project, }); MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/environments/`, method: 'GET', body: [], }); MockApiClient.addMockResponse({ url: `/organizations/${org.slug}/users/`, method: 'GET', body: [], }); }); afterEach(function () { window.location.assign.mockRestore(); MockApiClient.clearMockResponses(); addSuccessMessage.mockReset(); addErrorMessage.mockReset(); if (wrapper?.length) { wrapper.unmount(); wrapper = undefined; } if (modal?.length) { modal.unmount(); modal = undefined; } }); it('renders form fields', function () { wrapper = mountWithTheme( ); expect(wrapper.find('Input[name="name"]').prop('value')).toBe('Project Name'); expect(wrapper.find('Input[name="subjectPrefix"]').prop('value')).toBe('[my-org]'); expect(wrapper.find('RangeSlider[name="resolveAge"]').prop('value')).toBe(48); expect(wrapper.find('TextArea[name="allowedDomains"]').prop('value')).toBe( 'example.com\nhttps://example.com' ); expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isDisabled')).toBe( false ); expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isActive')).toBeTruthy(); expect(wrapper.find('Input[name="securityToken"]').prop('value')).toBe( 'security-token' ); expect(wrapper.find('Input[name="securityTokenHeader"]').prop('value')).toBe( 'x-security-header' ); expect(wrapper.find('Switch[name="verifySSL"]').prop('isActive')).toBeTruthy(); }); it('disables scrapeJavaScript when equivalent org setting is false', function () { routerContext.context.organization.scrapeJavaScript = false; wrapper = mountWithTheme( , routerContext ); expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isDisabled')).toBe(true); expect(wrapper.find('Switch[name="scrapeJavaScript"]').prop('isActive')).toBeFalsy(); }); it('project admins can remove project', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/`, method: 'DELETE', }); wrapper = mountWithTheme( ); const removeBtn = wrapper.find('.ref-remove-project').first(); expect(removeBtn.prop('children')).toBe('Remove Project'); // Click button removeBtn.simulate('click'); // Confirm Modal modal = await mountGlobalModal(); modal.find('Button[priority="danger"]').simulate('click'); expect(deleteMock).toHaveBeenCalled(); expect(removePageFiltersStorage).toHaveBeenCalledWith('org-slug'); }); it('project admins can transfer project', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/transfer/`, method: 'POST', }); wrapper = mountWithTheme( ); const removeBtn = wrapper.find('.ref-transfer-project').first(); expect(removeBtn.prop('children')).toBe('Transfer Project'); // Click button removeBtn.simulate('click'); // Confirm Modal modal = await mountGlobalModal(); modal .find('input[name="email"]') .simulate('change', {target: {value: 'billy@sentry.io'}}); modal.find('Modal Button[priority="danger"]').simulate('click'); await act(tick); await modal.update(); expect(addSuccessMessage).toHaveBeenCalled(); expect(deleteMock).toHaveBeenCalledWith( `/projects/${org.slug}/${project.slug}/transfer/`, expect.objectContaining({ method: 'POST', data: { email: 'billy@sentry.io', }, }) ); }); it('handles errors on transfer project', async function () { const deleteMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/transfer/`, method: 'POST', statusCode: 400, body: {detail: 'An organization owner could not be found'}, }); wrapper = mountWithTheme( ); const removeBtn = wrapper.find('.ref-transfer-project').first(); expect(removeBtn.prop('children')).toBe('Transfer Project'); // Click button removeBtn.simulate('click'); // Confirm Modal modal = await mountGlobalModal(); modal .find('input[name="email"]') .simulate('change', {target: {value: 'billy@sentry.io'}}); modal.find('Modal Button[priority="danger"]').simulate('click'); await act(tick); await modal.update(); expect(deleteMock).toHaveBeenCalled(); expect(addSuccessMessage).not.toHaveBeenCalled(); expect(addErrorMessage).toHaveBeenCalled(); const content = mountWithTheme(addErrorMessage.mock.calls[0][0]); expect(content.text()).toEqual( expect.stringContaining('An organization owner could not be found') ); }); it('displays transfer/remove message for non-admins', function () { routerContext.context.organization.access = ['org:read']; wrapper = mountWithTheme( , routerContext ); expect(wrapper.html()).toContain( 'You do not have the required permission to remove this project.' ); expect(wrapper.html()).toContain( 'You do not have the required permission to transfer this project.' ); }); it('disables the form for users without write permissions', function () { routerContext.context.organization.access = ['org:read']; wrapper = mountWithTheme( , routerContext ); expect(wrapper.find('FormField[disabled=false]')).toHaveLength(0); expect(wrapper.find('Alert').first().text()).toBe( 'These settings can only be edited by users with the organization owner, manager, or admin role.' ); }); it('changing project platform updates ProjectsStore', async function () { const params = {orgId: org.slug, projectId: project.slug}; act(() => ProjectsStore.loadInitialData([project])); putMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/`, method: 'PUT', body: { ...project, platform: 'javascript', }, }); wrapper = mountWithTheme( , routerContext ); await act(tick); wrapper.update(); // Change slug to new-slug selectByValue(wrapper, 'javascript'); // Slug does not save on blur expect(putMock).toHaveBeenCalled(); await act(tick); wrapper.update(); // updates ProjectsStore expect(ProjectsStore.itemsById['2'].platform).toBe('javascript'); }); it('changing name updates ProjectsStore', async function () { const params = {orgId: org.slug, projectId: project.slug}; act(() => ProjectsStore.loadInitialData([project])); putMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/`, method: 'PUT', body: { ...project, slug: 'new-project', }, }); wrapper = mountWithTheme( , routerContext ); await act(tick); wrapper.update(); // Change slug to new-slug wrapper .find('input[name="name"]') .simulate('change', {target: {value: 'New Project'}}) .simulate('blur'); // Slug does not save on blur expect(putMock).not.toHaveBeenCalled(); wrapper.find('Alert button[aria-label="Save"]').simulate('click'); // fetches new slug const newProjectGet = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/new-project/`, method: 'GET', body: {...project, slug: 'new-project'}, }); const newProjectMembers = MockApiClient.addMockResponse({ url: `/organizations/${org.slug}/users/`, method: 'GET', body: [], }); await act(tick); wrapper.update(); // updates ProjectsStore expect(ProjectsStore.itemsById['2'].slug).toBe('new-project'); expect(browserHistory.replace).toHaveBeenCalled(); expect(wrapper.find('Input[name="name"]').prop('value')).toBe('new-project'); wrapper.setProps({ projectId: 'new-project', }); await act(tick); wrapper.update(); expect(newProjectGet).toHaveBeenCalled(); expect(newProjectMembers).toHaveBeenCalled(); }); describe('Non-"save on blur" Field', function () { beforeEach(function () { const params = {orgId: org.slug, projectId: project.slug}; act(() => ProjectsStore.loadInitialData([project])); putMock = MockApiClient.addMockResponse({ url: `/projects/${org.slug}/${project.slug}/`, method: 'PUT', body: { ...project, slug: 'new-project', }, }); wrapper = mountWithTheme( , routerContext ); }); afterEach(() => { wrapper?.unmount(); modal?.unmount(); }); it('can cancel unsaved changes for a field', async function () { await act(tick); wrapper.update(); // Initially does not have "Cancel" button expect(wrapper.find('Alert button[aria-label="Cancel"]')).toHaveLength(0); // Has initial value expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(19); // Change value wrapper .find('input[name="resolveAge"]') .simulate('input', {target: {value: 12}}) .simulate('mouseUp'); // Has updated value expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(12); // Has "Cancel" button visible expect(wrapper.find('Alert button[aria-label="Cancel"]')).toHaveLength(1); // Click cancel wrapper.find('Alert button[aria-label="Cancel"]').simulate('click'); wrapper.update(); // Cancel row should disappear expect(wrapper.find('Alert button[aria-label="Cancel"]')).toHaveLength(0); // Value should be reverted expect(wrapper.find('input[name="resolveAge"]').prop('value')).toBe(19); // PUT should not be called expect(putMock).not.toHaveBeenCalled(); }); it('saves when value is changed and "Save" clicked', async function () { // This test has been flaky and using act() isn't removing the flakyness. await act(tick); wrapper.update(); // Initially does not have "Save" button expect(wrapper.find('Alert button[aria-label="Save"]')).toHaveLength(0); // Change value wrapper .find('input[name="resolveAge"]') .simulate('input', {target: {value: 12}}) .simulate('mouseUp'); await act(tick); wrapper.update(); // Has "Save" button visible expect(wrapper.find('Alert button[aria-label="Save"]')).toHaveLength(1); // Should not have put mock called yet expect(putMock).not.toHaveBeenCalled(); // Click "Save" wrapper.find('Alert button[aria-label="Save"]').simulate('click'); await act(tick); wrapper.update(); // API endpoint should have been called expect(putMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ data: { resolveAge: 12, }, }) ); // Should hide "Save" button after saving await act(tick); wrapper.update(); expect(wrapper.find('Alert button[aria-label="Save"]')).toHaveLength(0); }); }); });