123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
- );
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
- );
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
- );
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />
- );
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
- 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(
- <ProjectGeneralSettings params={{orgId: org.slug, projectId: project.slug}} />,
- 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(
- <ProjectContext orgId={org.slug} projectId={project.slug}>
- <ProjectGeneralSettings
- routes={[]}
- location={routerContext.context.location}
- params={params}
- />
- </ProjectContext>,
- 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(
- <ProjectContext orgId={org.slug} projectId={project.slug}>
- <ProjectGeneralSettings
- routes={[]}
- location={routerContext.context.location}
- params={params}
- />
- </ProjectContext>,
- 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(
- <ProjectContext orgId={org.slug} projectId={project.slug}>
- <ProjectGeneralSettings
- routes={[]}
- location={routerContext.context.location}
- params={params}
- />
- </ProjectContext>,
- 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);
- });
- });
- });
|