diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-28 21:09:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-28 21:09:35 +0300 |
commit | 95e18e32833de71b46d73ead66c8f13e261af3f4 (patch) | |
tree | bf61062dc1ae8ec2a25b28cd6385190661d3b37c /spec | |
parent | 37ae6b54ba524c438d1b756ce3ca29bbcec4e897 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
32 files changed, 911 insertions, 667 deletions
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb index 691716d3576..d3e02d43813 100644 --- a/spec/features/snippets/search_snippets_spec.rb +++ b/spec/features/snippets/search_snippets_spec.rb @@ -11,7 +11,7 @@ describe 'Search Snippets' do visit dashboard_snippets_path submit_search('Middle') - select_search_scope('Titles and Filenames') + select_search_scope('Titles and Descriptions') expect(page).to have_link(public_snippet.title) expect(page).to have_link(private_snippet.title) diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 42211f7ac9d..b6f2c7bb992 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -174,15 +174,16 @@ describe MergeRequestsFinder do deployment1 = create( :deployment, project: project_with_repo, - sha: project_with_repo.commit.id, - merge_requests: [merge_request1, merge_request2] + sha: project_with_repo.commit.id ) - create( + deployment2 = create( :deployment, project: project_with_repo, - sha: project_with_repo.commit.id, - merge_requests: [merge_request3] + sha: project_with_repo.commit.id ) + deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id])) + deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id)) + params = { deployment_id: deployment1.id } merge_requests = described_class.new(user, params).execute diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js new file mode 100644 index 00000000000..5ca620c24cf --- /dev/null +++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js @@ -0,0 +1,17 @@ +export const Editor = { + props: { + initialValue: { + type: String, + required: true, + }, + }, + render(h) { + return h('div'); + }, +}; + +export const Viewer = { + render(h) { + return h('div'); + }, +}; diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index 05a44138275..116851a9903 100644 --- a/spec/frontend/boards/boards_store_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -1041,6 +1041,66 @@ describe('boardsStore', () => { }); }); + describe('addListIssue', () => { + let list; + const issue1 = new ListIssue({ + title: 'Testing', + id: 2, + iid: 2, + confidential: false, + labels: [ + { + color: '#ff0000', + description: 'testing;', + id: 5000, + priority: undefined, + textColor: 'white', + title: 'Test', + }, + ], + assignees: [], + }); + const issue2 = new ListIssue({ + title: 'Testing', + id: 1, + iid: 1, + confidential: false, + labels: [ + { + id: 1, + title: 'test', + color: 'red', + description: 'testing', + }, + ], + assignees: [ + { + id: 1, + name: 'name', + username: 'username', + avatar_url: 'http://avatar_url', + }, + ], + real_path: 'path/to/issue', + }); + + beforeEach(() => { + list = new List(listObj); + list.addIssue(issue1); + setupDefaultResponses(); + }); + + it('adds issues that are not already on the list', () => { + expect(list.findIssue(issue2.id)).toBe(undefined); + expect(list.issues).toEqual([issue1]); + + boardsStore.addListIssue(list, issue2); + expect(list.findIssue(issue2.id)).toBe(issue2); + expect(list.issues.length).toBe(2); + expect(list.issues).toEqual([issue1, issue2]); + }); + }); + describe('updateIssue', () => { let issue; let patchSpy; diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js index 1adcdab272a..422332bab28 100644 --- a/spec/frontend/diffs/store/utils_spec.js +++ b/spec/frontend/diffs/store/utils_spec.js @@ -503,11 +503,16 @@ describe('DiffsStoreUtils', () => { }, }; + // When multi line comments are fully implemented `line_code` will be + // included in all requests. Until then we need to ensure the logic does + // not change when it is included only in the "comparison" argument. + const lineRange = { start_line_code: 'abc_1_1', end_line_code: 'abc_1_2' }; + it('returns true when the discussion is up to date', () => { expect( utils.isDiscussionApplicableToLine({ discussion: discussions.upToDateDiscussion1, - diffPosition, + diffPosition: { ...diffPosition, line_range: lineRange }, latestDiff: true, }), ).toBe(true); @@ -517,7 +522,7 @@ describe('DiffsStoreUtils', () => { expect( utils.isDiscussionApplicableToLine({ discussion: discussions.outDatedDiscussion1, - diffPosition, + diffPosition: { ...diffPosition, line_range: lineRange }, latestDiff: true, }), ).toBe(false); @@ -534,6 +539,7 @@ describe('DiffsStoreUtils', () => { diffPosition: { ...diffPosition, lineCode: 'ABC_1', + line_range: lineRange, }, latestDiff: true, }), @@ -551,6 +557,7 @@ describe('DiffsStoreUtils', () => { diffPosition: { ...diffPosition, line_code: 'ABC_1', + line_range: lineRange, }, latestDiff: true, }), @@ -568,6 +575,7 @@ describe('DiffsStoreUtils', () => { diffPosition: { ...diffPosition, lineCode: 'ABC_1', + line_range: lineRange, }, latestDiff: false, }), diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js index ba35f7bf7c6..2c3c3e3267a 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js @@ -1,6 +1,4 @@ import $ from 'jquery'; -import MockAdapter from 'axios-mock-adapter'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import axios from '~/lib/utils/axios_utils'; import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper'; import { togglePopover } from '~/shared/popover'; @@ -17,34 +15,23 @@ describe('feature highlight helper', () => { }); describe('dismiss', () => { - let mock; const context = { hide: () => {}, attr: () => '/-/callouts/dismiss', }; beforeEach(() => { - mock = new MockAdapter(axios); - - spyOn(togglePopover, 'call').and.callFake(() => {}); - spyOn(context, 'hide').and.callFake(() => {}); + jest.spyOn(axios, 'post').mockResolvedValue(); + jest.spyOn(togglePopover, 'call').mockImplementation(() => {}); + jest.spyOn(context, 'hide').mockImplementation(() => {}); dismiss.call(context); }); - afterEach(() => { - mock.restore(); - }); - - it('calls persistent dismissal endpoint', done => { - const spy = jasmine.createSpy('dismiss-endpoint-hit'); - mock.onPost('/-/callouts/dismiss').reply(spy); - - getSetTimeoutPromise() - .then(() => { - expect(spy).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); + it('calls persistent dismissal endpoint', () => { + expect(axios.post).toHaveBeenCalledWith( + '/-/callouts/dismiss', + expect.objectContaining({ feature_name: undefined }), + ); }); it('calls hide popover', () => { @@ -65,7 +52,7 @@ describe('feature highlight helper', () => { }, }; - spyOn($.fn, 'on').and.callFake(event => { + jest.spyOn($.fn, 'on').mockImplementation(event => { expect(event).toEqual('click'); done(); }); diff --git a/spec/frontend/feature_highlight/feature_highlight_options_spec.js b/spec/frontend/feature_highlight/feature_highlight_options_spec.js index 8b75c46fd4c..f82f984cb7f 100644 --- a/spec/frontend/feature_highlight/feature_highlight_options_spec.js +++ b/spec/frontend/feature_highlight/feature_highlight_options_spec.js @@ -3,34 +3,20 @@ import domContentLoaded from '~/feature_highlight/feature_highlight_options'; describe('feature highlight options', () => { describe('domContentLoaded', () => { - it('should not call highlightFeatures when breakpoint is xs', () => { - jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should not call highlightFeatures when breakpoint is sm', () => { - jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should not call highlightFeatures when breakpoint is md', () => { - jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should not call highlightFeatures when breakpoint is not xl', () => { - jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should call highlightFeatures when breakpoint is xl', () => { - jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl'); - - expect(domContentLoaded()).toBe(true); - }); + it.each` + breakPoint | shouldCall + ${'xs'} | ${false} + ${'sm'} | ${false} + ${'md'} | ${false} + ${'lg'} | ${false} + ${'xl'} | ${true} + `( + 'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall', + ({ breakPoint, shouldCall }) => { + jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint); + + expect(domContentLoaded()).toBe(shouldCall); + }, + ); }); }); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/frontend/feature_highlight/feature_highlight_spec.js index 40ac4bbb6a0..79c4050c8c4 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ b/spec/frontend/feature_highlight/feature_highlight_spec.js @@ -4,6 +4,8 @@ import * as featureHighlight from '~/feature_highlight/feature_highlight'; import * as popover from '~/shared/popover'; import axios from '~/lib/utils/axios_utils'; +jest.mock('~/shared/popover'); + describe('feature highlight', () => { beforeEach(() => { setFixtures(` @@ -28,7 +30,7 @@ describe('feature highlight', () => { beforeEach(() => { mock = new MockAdapter(axios); mock.onGet('/test').reply(200); - spyOn(window, 'addEventListener'); + jest.spyOn(window, 'addEventListener').mockImplementation(() => {}); featureHighlight.setupFeatureHighlightPopover('test', 0); }); @@ -44,27 +46,21 @@ describe('feature highlight', () => { }); it('setup mouseenter', () => { - const toggleSpy = spyOn(popover.togglePopover, 'call'); $(selector).trigger('mouseenter'); - expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); + expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object)); }); - it('setup debounced mouseleave', done => { - const toggleSpy = spyOn(popover.togglePopover, 'call'); + it('setup debounced mouseleave', () => { $(selector).trigger('mouseleave'); - // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce - setTimeout(() => { - expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false); - done(); - }, 0); + expect(popover.debouncedMouseleave).toHaveBeenCalled(); }); it('setup show.bs.popover', () => { $(selector).trigger('show.bs.popover'); - expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), { + expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), { once: true, }); }); @@ -72,23 +68,6 @@ describe('feature highlight', () => { it('removes disabled attribute', () => { expect($('.js-feature-highlight').is(':disabled')).toEqual(false); }); - - it('displays popover', () => { - expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy(); - $(selector).trigger('mouseenter'); - - expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy(); - }); - - it('toggles when clicked', () => { - $(selector).trigger('mouseenter'); - const popoverId = $(selector).attr('aria-describedby'); - const toggleSpy = spyOn(popover.togglePopover, 'call'); - - $(`#${popoverId} .dismiss-feature-highlight`).click(); - - expect(toggleSpy).toHaveBeenCalled(); - }); }); describe('findHighestPriorityFeature', () => { diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js new file mode 100644 index 00000000000..66f9237ce97 --- /dev/null +++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js @@ -0,0 +1,31 @@ +import { shallowMount } from '@vue/test-utils'; +import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; + +describe('EditFormButtons', () => { + let wrapper; + + const mountComponent = propsData => shallowMount(EditFormButtons, { propsData }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('displays "Unlock" when locked', () => { + wrapper = mountComponent({ + isLocked: true, + updateLockedAttribute: () => {}, + }); + + expect(wrapper.text()).toContain('Unlock'); + }); + + it('displays "Lock" when unlocked', () => { + wrapper = mountComponent({ + isLocked: false, + updateLockedAttribute: () => {}, + }); + + expect(wrapper.text()).toContain('Lock'); + }); +}); diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js new file mode 100644 index 00000000000..ebe94582588 --- /dev/null +++ b/spec/frontend/sidebar/participants_spec.js @@ -0,0 +1,206 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Participants from '~/sidebar/components/participants/participants.vue'; + +const PARTICIPANT = { + id: 1, + state: 'active', + username: 'marcene', + name: 'Allie Will', + web_url: 'foo.com', + avatar_url: 'gravatar.com/avatar/xxx', +}; + +const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; + +describe('Participants', () => { + let wrapper; + + const getMoreParticipantsButton = () => wrapper.find('button'); + + const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]'); + + const mountComponent = propsData => + shallowMount(Participants, { + propsData, + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('collapsed sidebar state', () => { + it('shows loading spinner when loading', () => { + wrapper = mountComponent({ + loading: true, + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('does not show loading spinner not loading', () => { + wrapper = mountComponent({ + loading: false, + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(false); + }); + + it('shows participant count when given', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + }); + + expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + }); + + it('shows full participant count when there are hidden participants', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 1, + }); + + expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`); + }); + }); + + describe('expanded sidebar state', () => { + it('shows loading spinner when loading', () => { + wrapper = mountComponent({ + loading: true, + }); + + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + }); + + it('when only showing visible participants, shows an avatar only for each participant under the limit', () => { + const numberOfLessParticipants = 2; + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants, + }); + + wrapper.setData({ + isShowingMoreParticipants: false, + }); + + return Vue.nextTick().then(() => { + expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants); + }); + }); + + it('when only showing all participants, each has an avatar', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 2, + }); + + wrapper.setData({ + isShowingMoreParticipants: true, + }); + + return Vue.nextTick().then(() => { + expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length); + }); + }); + + it('does not have more participants link when they can all be shown', () => { + const numberOfLessParticipants = 100; + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants, + }); + + expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants); + expect(getMoreParticipantsButton().exists()).toBe(false); + }); + + it('when too many participants, has more participants link to show more', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 2, + }); + + wrapper.setData({ + isShowingMoreParticipants: false, + }); + + return Vue.nextTick().then(() => { + expect(getMoreParticipantsButton().text()).toBe('+ 1 more'); + }); + }); + + it('when too many participants and already showing them, has more participants link to show less', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 2, + }); + + wrapper.setData({ + isShowingMoreParticipants: true, + }); + + return Vue.nextTick().then(() => { + expect(getMoreParticipantsButton().text()).toBe('- show less'); + }); + }); + + it('clicking more participants link emits event', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 2, + }); + + expect(wrapper.vm.isShowingMoreParticipants).toBe(false); + + getMoreParticipantsButton().trigger('click'); + + expect(wrapper.vm.isShowingMoreParticipants).toBe(true); + }); + + it('clicking on participants icon emits `toggleSidebar` event', () => { + wrapper = mountComponent({ + loading: false, + participants: PARTICIPANT_LIST, + numberOfLessParticipants: 2, + }); + + const spy = jest.spyOn(wrapper.vm, '$emit'); + + wrapper.find('.sidebar-collapsed-icon').trigger('click'); + + return Vue.nextTick(() => { + expect(spy).toHaveBeenCalledWith('toggleSidebar'); + + spy.mockRestore(); + }); + }); + }); + + describe('when not showing participants label', () => { + beforeEach(() => { + wrapper = mountComponent({ + participants: PARTICIPANT_LIST, + showParticipantLabel: false, + }); + }); + + it('does not show sidebar collapsed icon', () => { + expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false); + }); + + it('does not show participants label title', () => { + expect(wrapper.contains('.title')).toBe(false); + }); + }); +}); diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js new file mode 100644 index 00000000000..0892d452966 --- /dev/null +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -0,0 +1,135 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import * as urlUtility from '~/lib/utils/url_utility'; +import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from './mock_data'; + +describe('Sidebar mediator', () => { + const { mediator: mediatorMockData } = Mock; + let mock; + let mediator; + + beforeEach(() => { + mock = new MockAdapter(axios); + mediator = new SidebarMediator(mediatorMockData); + }); + + afterEach(() => { + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + mock.restore(); + }); + + it('assigns yourself ', () => { + mediator.assignYourself(); + + expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser); + expect(mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser); + }); + + it('saves assignees', () => { + mock.onPut(mediatorMockData.endpoint).reply(200, {}); + + return mediator.saveAssignees('issue[assignee_ids]').then(resp => { + expect(resp.status).toEqual(200); + }); + }); + + it('fetches the data', () => { + const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; + mock.onGet(mediatorMockData.endpoint).reply(200, mockData); + + const mockGraphQlData = Mock.graphQlResponseData; + const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({ + data: mockGraphQlData, + }); + const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve()); + + return mediator.fetch().then(() => { + expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData); + + spy.mockRestore(); + graphQlSpy.mockRestore(); + }); + }); + + it('processes fetched data', () => { + const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; + mediator.processFetchedData(mockData); + + expect(mediator.store.assignees).toEqual(mockData.assignees); + expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate); + expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent); + expect(mediator.store.participants).toEqual(mockData.participants); + expect(mediator.store.subscribed).toEqual(mockData.subscribed); + expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate); + expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent); + }); + + it('sets moveToProjectId', () => { + const projectId = 7; + const spy = jest.spyOn(mediator.store, 'setMoveToProjectId').mockReturnValue(Promise.resolve()); + + mediator.setMoveToProjectId(projectId); + + expect(spy).toHaveBeenCalledWith(projectId); + + spy.mockRestore(); + }); + + it('fetches autocomplete projects', () => { + const searchTerm = 'foo'; + mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {}); + const getterSpy = jest + .spyOn(mediator.service, 'getProjectsAutocomplete') + .mockReturnValue(Promise.resolve({ data: {} })); + const setterSpy = jest + .spyOn(mediator.store, 'setAutocompleteProjects') + .mockReturnValue(Promise.resolve()); + + return mediator.fetchAutocompleteProjects(searchTerm).then(() => { + expect(getterSpy).toHaveBeenCalledWith(searchTerm); + expect(setterSpy).toHaveBeenCalled(); + + getterSpy.mockRestore(); + setterSpy.mockRestore(); + }); + }); + + it('moves issue', () => { + const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint]; + const moveToProjectId = 7; + mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData); + mediator.store.setMoveToProjectId(moveToProjectId); + const moveIssueSpy = jest + .spyOn(mediator.service, 'moveIssue') + .mockReturnValue(Promise.resolve({ data: { web_url: mockData.web_url } })); + const urlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); + + return mediator.moveIssue().then(() => { + expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId); + expect(urlSpy).toHaveBeenCalledWith(mockData.web_url); + + moveIssueSpy.mockRestore(); + urlSpy.mockRestore(); + }); + }); + + it('toggle subscription', () => { + mediator.store.setSubscribedState(false); + mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {}); + const spy = jest + .spyOn(mediator.service, 'toggleSubscription') + .mockReturnValue(Promise.resolve()); + + return mediator.toggleSubscription().then(() => { + expect(spy).toHaveBeenCalled(); + expect(mediator.store.subscribed).toEqual(true); + + spy.mockRestore(); + }); + }); +}); diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js new file mode 100644 index 00000000000..18aaeabe3dd --- /dev/null +++ b/spec/frontend/sidebar/sidebar_subscriptions_spec.js @@ -0,0 +1,36 @@ +import { shallowMount } from '@vue/test-utils'; +import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarService from '~/sidebar/services/sidebar_service'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from './mock_data'; + +describe('Sidebar Subscriptions', () => { + let wrapper; + let mediator; + + beforeEach(() => { + mediator = new SidebarMediator(Mock.mediator); + wrapper = shallowMount(SidebarSubscriptions, { + propsData: { + mediator, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + }); + + it('calls the mediator toggleSubscription on event', () => { + const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve()); + + wrapper.vm.onToggleSubscription(); + + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); +}); diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js new file mode 100644 index 00000000000..cce35666985 --- /dev/null +++ b/spec/frontend/sidebar/subscriptions_spec.js @@ -0,0 +1,106 @@ +import { shallowMount } from '@vue/test-utils'; +import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; +import eventHub from '~/sidebar/event_hub'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; + +describe('Subscriptions', () => { + let wrapper; + + const findToggleButton = () => wrapper.find(ToggleButton); + + const mountComponent = propsData => + shallowMount(Subscriptions, { + propsData, + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('shows loading spinner when loading', () => { + wrapper = mountComponent({ + loading: true, + subscribed: undefined, + }); + + expect(findToggleButton().attributes('isloading')).toBe('true'); + }); + + it('is toggled "off" when currently not subscribed', () => { + wrapper = mountComponent({ + subscribed: false, + }); + + expect(findToggleButton().attributes('value')).toBeFalsy(); + }); + + it('is toggled "on" when currently subscribed', () => { + wrapper = mountComponent({ + subscribed: true, + }); + + expect(findToggleButton().attributes('value')).toBe('true'); + }); + + it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => { + const id = 42; + wrapper = mountComponent({ subscribed: true, id }); + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit'); + + wrapper.vm.toggleSubscription(); + + expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id); + expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id); + eventHubSpy.mockRestore(); + wrapperEmitSpy.mockRestore(); + }); + + it('tracks the event when toggled', () => { + wrapper = mountComponent({ subscribed: true }); + + const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track'); + + wrapper.vm.toggleSubscription(); + + expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', { + property: 'notifications', + value: 0, + }); + wrapperTrackSpy.mockRestore(); + }); + + it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => { + wrapper = mountComponent({ subscribed: true }); + const spy = jest.spyOn(wrapper.vm, '$emit'); + + wrapper.vm.onClickCollapsedIcon(); + + expect(spy).toHaveBeenCalledWith('toggleSidebar'); + spy.mockRestore(); + }); + + describe('given project emails are disabled', () => { + const subscribeDisabledDescription = 'Notifications have been disabled'; + + beforeEach(() => { + wrapper = mountComponent({ + subscribed: false, + projectEmailsDisabled: true, + subscribeDisabledDescription, + }); + }); + + it('sets the correct display text', () => { + expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription); + expect(wrapper.find({ ref: 'tooltip' }).attributes('data-original-title')).toBe( + subscribeDisabledDescription, + ); + }); + + it('does not render the toggle button', () => { + expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js new file mode 100644 index 00000000000..eb2baac3e76 --- /dev/null +++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js @@ -0,0 +1,37 @@ +import { shallowMount } from '@vue/test-utils'; +import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; + +describe('Rich Content Editor', () => { + let wrapper; + + const value = '## Some Markdown'; + const findEditor = () => wrapper.find({ ref: 'editor' }); + + beforeEach(() => { + wrapper = shallowMount(RichContentEditor, { + propsData: { value }, + }); + }); + + describe('when content is loaded', () => { + it('renders an editor', () => { + expect(findEditor().exists()).toBe(true); + }); + + it('renders the correct content', () => { + expect(findEditor().props().initialValue).toBe(value); + }); + }); + + describe('when content is changed', () => { + it('emits an input event with the changed content', () => { + const changedMarkdown = '## Changed Markdown'; + const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown); + + findEditor().setMethods({ invoke: getMarkdownMock }); + findEditor().vm.$emit('change'); + + expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown); + }); + }); +}); diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 18c94602596..6a06b012c6c 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -112,7 +112,6 @@ describe SearchHelper do 'milestones' | 'milestone' 'notes' | 'comment' 'projects' | 'project' - 'snippet_blobs' | 'snippet result' 'snippet_titles' | 'snippet' 'users' | 'user' 'wiki_blobs' | 'wiki result' diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js deleted file mode 100644 index c532554efb4..00000000000 --- a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; - -describe('EditFormButtons', () => { - let vm1; - let vm2; - - beforeEach(() => { - const Component = Vue.extend(editFormButtons); - const toggleForm = () => {}; - const updateLockedAttribute = () => {}; - - vm1 = mountComponent(Component, { - isLocked: true, - toggleForm, - updateLockedAttribute, - }); - - vm2 = mountComponent(Component, { - isLocked: false, - toggleForm, - updateLockedAttribute, - }); - }); - - it('renders unlock or lock text based on locked state', () => { - expect(vm1.$el.innerHTML.includes('Unlock')).toBe(true); - - expect(vm2.$el.innerHTML.includes('Lock')).toBe(true); - }); -}); diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js deleted file mode 100644 index 7e80e86f8ca..00000000000 --- a/spec/javascripts/sidebar/participants_spec.js +++ /dev/null @@ -1,202 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import participants from '~/sidebar/components/participants/participants.vue'; - -const PARTICIPANT = { - id: 1, - state: 'active', - username: 'marcene', - name: 'Allie Will', - web_url: 'foo.com', - avatar_url: 'gravatar.com/avatar/xxx', -}; - -const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; - -describe('Participants', function() { - let vm; - let Participants; - - beforeEach(() => { - Participants = Vue.extend(participants); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('collapsed sidebar state', () => { - it('shows loading spinner when loading', () => { - vm = mountComponent(Participants, { - loading: true, - }); - - expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined(); - }); - - it('shows participant count when given', () => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - }); - const countEl = vm.$el.querySelector('.js-participants-collapsed-count'); - - expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`); - }); - - it('shows full participant count when there are hidden participants', () => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 1, - }); - const countEl = vm.$el.querySelector('.js-participants-collapsed-count'); - - expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`); - }); - }); - - describe('expanded sidebar state', () => { - it('shows loading spinner when loading', () => { - vm = mountComponent(Participants, { - loading: true, - }); - - expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined(); - }); - - it('when only showing visible participants, shows an avatar only for each participant under the limit', done => { - const numberOfLessParticipants = 2; - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - vm.isShowingMoreParticipants = false; - - Vue.nextTick() - .then(() => { - const participantEls = vm.$el.querySelectorAll('.js-participants-author'); - - expect(participantEls.length).toBe(numberOfLessParticipants); - }) - .then(done) - .catch(done.fail); - }); - - it('when only showing all participants, each has an avatar', done => { - const numberOfLessParticipants = 2; - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - vm.isShowingMoreParticipants = true; - - Vue.nextTick() - .then(() => { - const participantEls = vm.$el.querySelectorAll('.js-participants-author'); - - expect(participantEls.length).toBe(PARTICIPANT_LIST.length); - }) - .then(done) - .catch(done.fail); - }); - - it('does not have more participants link when they can all be shown', () => { - const numberOfLessParticipants = 100; - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants, - }); - const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button'); - - expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants); - expect(moreParticipantLink).toBeNull(); - }); - - it('when too many participants, has more participants link to show more', done => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - vm.isShowingMoreParticipants = false; - - Vue.nextTick() - .then(() => { - const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button'); - - expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more'); - }) - .then(done) - .catch(done.fail); - }); - - it('when too many participants and already showing them, has more participants link to show less', done => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - vm.isShowingMoreParticipants = true; - - Vue.nextTick() - .then(() => { - const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button'); - - expect(moreParticipantLink.textContent.trim()).toBe('- show less'); - }) - .then(done) - .catch(done.fail); - }); - - it('clicking more participants link emits event', () => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button'); - - expect(vm.isShowingMoreParticipants).toBe(false); - - moreParticipantLink.click(); - - expect(vm.isShowingMoreParticipants).toBe(true); - }); - - it('clicking on participants icon emits `toggleSidebar` event', () => { - vm = mountComponent(Participants, { - loading: false, - participants: PARTICIPANT_LIST, - numberOfLessParticipants: 2, - }); - spyOn(vm, '$emit'); - - const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon'); - - participantsIconEl.click(); - - expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar'); - }); - }); - - describe('when not showing participants label', () => { - beforeEach(() => { - vm = mountComponent(Participants, { - participants: PARTICIPANT_LIST, - showParticipantLabel: false, - }); - }); - - it('does not show sidebar collapsed icon', () => { - expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBeTruthy(); - }); - - it('does not show participants label title', () => { - expect(vm.$el.querySelector('.title')).not.toBeTruthy(); - }); - }); -}); diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js deleted file mode 100644 index 2aa30fd1cc6..00000000000 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import SidebarMediator from '~/sidebar/sidebar_mediator'; -import SidebarStore from '~/sidebar/stores/sidebar_store'; -import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; -import Mock from './mock_data'; - -const { mediator: mediatorMockData } = Mock; - -describe('Sidebar mediator', function() { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - - this.mediator = new SidebarMediator(mediatorMockData); - }); - - afterEach(() => { - SidebarService.singleton = null; - SidebarStore.singleton = null; - SidebarMediator.singleton = null; - mock.restore(); - }); - - it('assigns yourself ', () => { - this.mediator.assignYourself(); - - expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser); - expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser); - }); - - it('saves assignees', done => { - mock.onPut(mediatorMockData.endpoint).reply(200, {}); - this.mediator - .saveAssignees('issue[assignee_ids]') - .then(resp => { - expect(resp.status).toEqual(200); - done(); - }) - .catch(done.fail); - }); - - it('fetches the data', done => { - const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; - mock.onGet(mediatorMockData.endpoint).reply(200, mockData); - - const mockGraphQlData = Mock.graphQlResponseData; - spyOn(gqClient, 'query').and.returnValue({ - data: mockGraphQlData, - }); - - spyOn(this.mediator, 'processFetchedData').and.callThrough(); - - this.mediator - .fetch() - .then(() => { - expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData, mockGraphQlData); - }) - .then(done) - .catch(done.fail); - }); - - it('processes fetched data', () => { - const mockData = Mock.responseMap.GET[mediatorMockData.endpoint]; - this.mediator.processFetchedData(mockData); - - expect(this.mediator.store.assignees).toEqual(mockData.assignees); - expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate); - expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent); - expect(this.mediator.store.participants).toEqual(mockData.participants); - expect(this.mediator.store.subscribed).toEqual(mockData.subscribed); - expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate); - expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent); - }); - - it('sets moveToProjectId', () => { - const projectId = 7; - spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough(); - - this.mediator.setMoveToProjectId(projectId); - - expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId); - }); - - it('fetches autocomplete projects', done => { - const searchTerm = 'foo'; - mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {}); - spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough(); - spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough(); - - this.mediator - .fetchAutocompleteProjects(searchTerm) - .then(() => { - expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm); - expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('moves issue', done => { - const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint]; - const moveToProjectId = 7; - mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData); - this.mediator.store.setMoveToProjectId(moveToProjectId); - spyOn(this.mediator.service, 'moveIssue').and.callThrough(); - const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl'); - - this.mediator - .moveIssue() - .then(() => { - expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); - expect(visitUrl).toHaveBeenCalledWith(mockData.web_url); - }) - .then(done) - .catch(done.fail); - }); - - it('toggle subscription', done => { - this.mediator.store.setSubscribedState(false); - mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {}); - spyOn(this.mediator.service, 'toggleSubscription').and.callThrough(); - - this.mediator - .toggleSubscription() - .then(() => { - expect(this.mediator.service.toggleSubscription).toHaveBeenCalled(); - expect(this.mediator.store.subscribed).toEqual(true); - }) - .then(done) - .catch(done.fail); - }); -}); diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js deleted file mode 100644 index ee4516f3bcd..00000000000 --- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue'; -import SidebarMediator from '~/sidebar/sidebar_mediator'; -import SidebarService from '~/sidebar/services/sidebar_service'; -import SidebarStore from '~/sidebar/stores/sidebar_store'; -import Mock from './mock_data'; - -describe('Sidebar Subscriptions', function() { - let vm; - let SidebarSubscriptions; - - beforeEach(() => { - SidebarSubscriptions = Vue.extend(sidebarSubscriptions); - // Set up the stores, services, etc - // eslint-disable-next-line no-new - new SidebarMediator(Mock.mediator); - }); - - afterEach(() => { - vm.$destroy(); - SidebarService.singleton = null; - SidebarStore.singleton = null; - SidebarMediator.singleton = null; - }); - - it('calls the mediator toggleSubscription on event', () => { - const mediator = new SidebarMediator(); - spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve()); - vm = mountComponent(SidebarSubscriptions, { - mediator, - }); - - vm.onToggleSubscription(); - - expect(mediator.toggleSubscription).toHaveBeenCalled(); - }); -}); diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js deleted file mode 100644 index cdb39efbef8..00000000000 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ /dev/null @@ -1,100 +0,0 @@ -import Vue from 'vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { mockTracking } from 'spec/helpers/tracking_helper'; -import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue'; -import eventHub from '~/sidebar/event_hub'; - -describe('Subscriptions', function() { - let vm; - let Subscriptions; - - beforeEach(() => { - Subscriptions = Vue.extend(subscriptions); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('shows loading spinner when loading', () => { - vm = mountComponent(Subscriptions, { - loading: true, - subscribed: undefined, - }); - - expect(vm.$refs.toggleButton.isLoading).toBe(true); - expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass( - 'is-loading', - ); - }); - - it('is toggled "off" when currently not subscribed', () => { - vm = mountComponent(Subscriptions, { - subscribed: false, - }); - - expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass( - 'is-checked', - ); - }); - - it('is toggled "on" when currently subscribed', () => { - vm = mountComponent(Subscriptions, { - subscribed: true, - }); - - expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass( - 'is-checked', - ); - }); - - it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => { - vm = mountComponent(Subscriptions, { subscribed: true }); - spyOn(eventHub, '$emit'); - spyOn(vm, '$emit'); - spyOn(vm, 'track'); - - vm.toggleSubscription(); - - expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object)); - expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object)); - }); - - it('tracks the event when toggled', () => { - vm = mountComponent(Subscriptions, { subscribed: true }); - const spy = mockTracking('_category_', vm.$el, spyOn); - vm.toggleSubscription(); - - expect(spy).toHaveBeenCalled(); - }); - - it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => { - vm = mountComponent(Subscriptions, { subscribed: true }); - spyOn(vm, '$emit'); - - vm.onClickCollapsedIcon(); - - expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar'); - }); - - describe('given project emails are disabled', () => { - const subscribeDisabledDescription = 'Notifications have been disabled'; - - beforeEach(() => { - vm = mountComponent(Subscriptions, { - subscribed: false, - projectEmailsDisabled: true, - subscribeDisabledDescription, - }); - }); - - it('sets the correct display text', () => { - expect(vm.$el.textContent).toContain(subscribeDisabledDescription); - expect(vm.$refs.tooltip.dataset.originalTitle).toBe(subscribeDisabledDescription); - }); - - it('does not render the toggle button', () => { - expect(vm.$refs.toggleButton).toBeUndefined(); - }); - }); -}); diff --git a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb new file mode 100644 index 00000000000..c3bb975727b --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20200312134637 do + let(:environments) { table(:environments) } + let(:merge_requests) { table(:merge_requests) } + let(:deployments) { table(:deployments) } + let(:deployment_merge_requests) { table(:deployment_merge_requests) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + subject(:migration) { described_class.new } + + it 'correctly backfills environment_id column' do + namespace = namespaces.create!(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + production = environments.create!(project_id: project.id, name: 'production', slug: 'production') + staging = environments.create!(project_id: project.id, name: 'staging', slug: 'staging') + + mr = merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id) + + deployment1 = deployments.create!(environment_id: staging.id, iid: 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1) + deployment2 = deployments.create!(environment_id: production.id, iid: 2, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1) + deployment3 = deployments.create!(environment_id: production.id, iid: 3, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1) + + # mr is tracked twice in production through deployment2 and deployment3 + deployment_merge_requests.create!(deployment_id: deployment1.id, merge_request_id: mr.id) + deployment_merge_requests.create!(deployment_id: deployment2.id, merge_request_id: mr.id) + deployment_merge_requests.create!(deployment_id: deployment3.id, merge_request_id: mr.id) + + expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3) + + migration.perform(1, mr.id) + + expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero + expect(deployment_merge_requests.count).to eq(2) + + production_deployments = deployment_merge_requests.where(environment_id: production.id) + expect(production_deployments.count).to eq(1) + expect(production_deployments.first.deployment_id).to eq(deployment2.id) + + expect(deployment_merge_requests.where(environment_id: staging.id).count).to eq(1) + end +end diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb index ba23c3828de..8929374fb87 100644 --- a/spec/lib/gitlab/danger/changelog_spec.rb +++ b/spec/lib/gitlab/danger/changelog_spec.rb @@ -86,14 +86,6 @@ describe Gitlab::Danger::Changelog do end end - describe '#presented_no_changelog_labels' do - subject { changelog.presented_no_changelog_labels } - - it 'returns the labels formatted' do - is_expected.to eq('~backstage, ~ci-build, ~meta') - end - end - describe '#ee_changelog?' do subject { changelog.ee_changelog? } diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 0f5b00e4bb5..c2c881fd589 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -399,9 +399,28 @@ describe Gitlab::Danger::Helper do end end + describe '#labels_list' do + let(:labels) { ['telemetry', 'telemetry::reviewed'] } + + it 'composes the labels string' do + expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"') + end + + context 'when passing a separator' do + it 'composes the labels string with the given separator' do + expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"') + end + end + + it 'returns empty string for empty array' do + expect(helper.labels_list([])).to eq('') + end + end + describe '#prepare_labels_for_mr' do it 'composes the labels string' do mr_labels = ['telemetry', 'telemetry::reviewed'] + expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"') end diff --git a/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb new file mode 100644 index 00000000000..296ae07cc21 --- /dev/null +++ b/spec/migrations/backfill_environment_id_on_deployment_merge_requests_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200312134637_backfill_environment_id_on_deployment_merge_requests.rb') + +describe BackfillEnvironmentIdOnDeploymentMergeRequests do + let(:environments) { table(:environments) } + let(:merge_requests) { table(:merge_requests) } + let(:deployments) { table(:deployments) } + let(:deployment_merge_requests) { table(:deployment_merge_requests) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + let(:migration_worker) { double('BackgroundMigrationWorker') } + + before do + stub_const('BackgroundMigrationWorker', migration_worker) + end + + it 'schedules nothing when there are no entries' do + expect(migration_worker).not_to receive(:perform_in) + + migrate! + end + + it 'batches the workload' do + stub_const("#{described_class.name}::BATCH_SIZE", 10) + + namespace = namespaces.create!(name: 'foo', path: 'foo') + project = projects.create!(namespace_id: namespace.id) + + environment = environments.create!(project_id: project.id, name: 'staging', slug: 'staging') + + # Batching is based on DeploymentMergeRequest.merge_request_id, in order to test it + # we must generate more than described_class::BATCH_SIZE merge requests, deployments, + # and deployment_merge_requests entries + entries = 13 + expect(entries).to be > described_class::BATCH_SIZE + + # merge requests and deployments bulk generation + mrs_params = [] + deployments_params = [] + entries.times do |i| + mrs_params << { source_branch: 'x', target_branch: 'master', target_project_id: project.id } + + deployments_params << { environment_id: environment.id, iid: i + 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1 } + end + + all_mrs = merge_requests.insert_all(mrs_params) + all_deployments = deployments.insert_all(deployments_params) + + # deployment_merge_requests bulk generation + dmr_params = [] + entries.times do |index| + mr_id = all_mrs.rows[index].first + deployment_id = all_deployments.rows[index].first + + dmr_params << { deployment_id: deployment_id, merge_request_id: mr_id } + end + + deployment_merge_requests.insert_all(dmr_params) + + first_batch_limit = dmr_params[described_class::BATCH_SIZE][:merge_request_id] + second_batch_limit = dmr_params.last[:merge_request_id] + + expect(migration_worker).to receive(:perform_in) + .with( + 0, + 'BackfillEnvironmentIdDeploymentMergeRequests', + [1, first_batch_limit] + ) + expect(migration_worker).to receive(:perform_in) + .with( + described_class::DELAY, + 'BackfillEnvironmentIdDeploymentMergeRequests', + [first_batch_limit + 1, second_batch_limit] + ) + + migrate! + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8ec2ed2bf77..1a168e564dd 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3700,41 +3700,41 @@ describe MergeRequest do describe '#recent_visible_deployments' do let(:merge_request) { create(:merge_request) } - let(:environment) do - create(:environment, project: merge_request.target_project) - end - it 'returns visible deployments' do + envs = create_list(:environment, 3, project: merge_request.target_project) + created = create( :deployment, :created, project: merge_request.target_project, - environment: environment + environment: envs[0] ) success = create( :deployment, :success, project: merge_request.target_project, - environment: environment + environment: envs[1] ) failed = create( :deployment, :failed, project: merge_request.target_project, - environment: environment + environment: envs[2] ) - merge_request.deployment_merge_requests.create!(deployment: created) - merge_request.deployment_merge_requests.create!(deployment: success) - merge_request.deployment_merge_requests.create!(deployment: failed) + merge_request_relation = MergeRequest.where(id: merge_request.id) + created.link_merge_requests(merge_request_relation) + success.link_merge_requests(merge_request_relation) + failed.link_merge_requests(merge_request_relation) expect(merge_request.recent_visible_deployments).to eq([failed, success]) end it 'only returns a limited number of deployments' do 20.times do + environment = create(:environment, project: merge_request.target_project) deploy = create( :deployment, :success, @@ -3742,7 +3742,7 @@ describe MergeRequest do environment: environment ) - merge_request.deployment_merge_requests.create!(deployment: deploy) + deploy.link_merge_requests(MergeRequest.where(id: merge_request.id)) end expect(merge_request.recent_visible_deployments.count).to eq(10) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 12f753d34d2..10da73104a4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -538,18 +538,6 @@ describe User, :do_not_mock_admin_mode do expect(user).to be_valid end - context 'when feature flag is turned off' do - before do - stub_feature_flags(email_restrictions: false) - end - - it 'does accept the email address' do - user = build(:user, email: 'info+1@test.com') - - expect(user).to be_valid - end - end - context 'when created_by_id is set' do it 'does accept the email address' do user = build(:user, email: 'info+1@test.com', created_by_id: 1) diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index b820b227fff..ef2415a0cde 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -439,7 +439,7 @@ describe API::Deployments do let!(:merge_request3) { create(:merge_request, source_project: project2, target_project: project2) } it 'returns the relevant merge requests linked to a deployment for a project' do - deployment.merge_requests << [merge_request1, merge_request2] + deployment.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id])) subject diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 3ec5f380390..5c925d2a32e 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -3,26 +3,26 @@ require 'spec_helper' describe API::Issues do - let_it_be(:user) { create(:user) } - let(:user2) { create(:user) } - let(:non_member) { create(:user) } - let_it_be(:guest) { create(:user) } - let_it_be(:author) { create(:author) } - let_it_be(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } - let(:issue_title) { 'foo' } - let(:issue_description) { 'closed' } - let(:no_milestone_title) { 'None' } - let(:any_milestone_title) { 'Any' } + let_it_be(:user2) { create(:user) } + let_it_be(:admin) { create(:user, :admin) } + let_it_be(:non_member) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:author) { create(:author) } + let_it_be(:assignee) { create(:assignee) } + let_it_be(:issue_title) { 'foo' } + let_it_be(:issue_description) { 'closed' } + let_it_be(:no_milestone_title) { 'None' } + let_it_be(:any_milestone_title) { 'Any' } before do stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) end describe 'GET /groups/:id/issues' do - let!(:group) { create(:group) } - let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) } - let!(:private_mrs_project) do + let_it_be(:group) { create(:group) } + let_it_be(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) } + let_it_be(:private_mrs_project) do create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE) end @@ -455,6 +455,29 @@ describe API::Issues do it_behaves_like 'labeled issues with labels and label_name params' end + context 'with archived projects' do + let_it_be(:archived_issue) do + create( + :issue, author: user, assignees: [user], + project: create(:project, :public, :archived, creator_id: user.id, namespace: group) + ) + end + + it 'returns only non archived projects issues' do + get api(base_url, user) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + it 'returns issues from archived projects if non_archived it set to false' do + get api(base_url, user), params: { non_archived: false } + + expect_paginated_array_response( + [archived_issue.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id] + ) + end + end + it 'returns an array of issues found by iids' do get api(base_url, user), params: { iids: [group_issue.iid] } diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 00169c1529f..06878f57d43 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -780,28 +780,20 @@ describe API::Issues do end context 'filtering by non_archived' do - let_it_be(:group1) { create(:group) } - let_it_be(:archived_project) { create(:project, :archived, namespace: group1) } - let_it_be(:active_project) { create(:project, namespace: group1) } - let_it_be(:issue1) { create(:issue, project: active_project) } - let_it_be(:issue2) { create(:issue, project: active_project) } - let_it_be(:issue3) { create(:issue, project: archived_project) } + let_it_be(:archived_project) { create(:project, :archived, creator_id: user.id, namespace: user.namespace) } + let_it_be(:archived_issue) { create(:issue, author: user, project: archived_project) } + let_it_be(:active_issue) { create(:issue, author: user, project: project) } - before do - archived_project.add_developer(user) - active_project.add_developer(user) - end - - it 'returns issues from non archived projects only by default' do - get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' } + it 'returns issues from non archived projects by default' do + get api('/issues', user) - expect_paginated_array_response([issue2.id, issue1.id]) + expect_paginated_array_response(active_issue.id, issue.id, closed_issue.id) end - it 'returns issues from archived and non archived projects when non_archived is false' do - get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' } + it 'returns issues from archived project with non_archived set as false' do + get api("/issues", user), params: { non_archived: false } - expect_paginated_array_response([issue3.id, issue2.id, issue1.id]) + expect_paginated_array_response(active_issue.id, archived_issue.id, issue.id, closed_issue.id) end end end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index e0a673514a8..0bdb9ea6bf9 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -129,16 +129,6 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/snippets' end - - context 'for snippet_blobs scope' do - before do - create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') - - get api('/search', user), params: { scope: 'snippet_blobs', search: 'content' } - end - - it_behaves_like 'response is correct', schema: 'public_api/v4/snippets' - end end end diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb index 419d74c298a..dfc3898af24 100644 --- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb +++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb @@ -33,5 +33,11 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do expect(cop.offenses.map(&:line)).to eq([1]) end end + + it 'does not register an offense when a `NOT VALID` foreign key is added' do + inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id, validate: false); end') + + expect(cop.offenses).to be_empty + end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 4465e1b1bd1..7a251e03e51 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -110,6 +110,31 @@ describe Issues::CreateService do end end + context 'when labels is nil' do + let(:opts) do + { title: 'Title', + description: 'Description', + labels: nil } + end + + it 'does not assign label' do + expect(issue.labels).to be_empty + end + end + + context 'when labels is nil and label_ids is present' do + let(:opts) do + { title: 'Title', + description: 'Description', + labels: nil, + label_ids: labels.map(&:id) } + end + + it 'assigns group labels' do + expect(issue.labels).to match_array labels + end + end + context 'when milestone belongs to different project' do let(:milestone) { create(:milestone) } |