diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-14 03:06:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-14 03:06:25 +0300 |
commit | a93dfc1b7e55b118b1cf4a67afeb46556292914c (patch) | |
tree | 65b874b7940d0d05c4ebedaef43b8a1009362651 /spec | |
parent | 188a57f93bba5953800de490fcc6246966a073fd (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/features/issues/user_uses_quick_actions_spec.rb | 1 | ||||
-rw-r--r-- | spec/frontend/notes/components/note_app_spec.js | 35 | ||||
-rw-r--r-- | spec/frontend/releases/components/milestone_list_spec.js | 56 | ||||
-rw-r--r-- | spec/frontend/releases/components/release_block_spec.js | 120 | ||||
-rw-r--r-- | spec/frontend/releases/mock_data.js | 97 | ||||
-rw-r--r-- | spec/javascripts/notes/mock_data.js | 20 | ||||
-rw-r--r-- | spec/javascripts/notes/stores/actions_spec.js | 121 | ||||
-rw-r--r-- | spec/javascripts/releases/components/release_block_spec.js | 168 | ||||
-rw-r--r-- | spec/services/issues/zoom_link_service_spec.rb | 243 | ||||
-rw-r--r-- | spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb | 111 |
10 files changed, 667 insertions, 305 deletions
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index 26979e943d0..09f07f8c908 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -42,5 +42,6 @@ describe 'Issues > User uses quick actions', :js do it_behaves_like 'create_merge_request quick action' it_behaves_like 'move quick action' + it_behaves_like 'zoom quick actions' end end diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js index 02fd30d5a15..d2c17310e9c 100644 --- a/spec/frontend/notes/components/note_app_spec.js +++ b/spec/frontend/notes/components/note_app_spec.js @@ -1,4 +1,6 @@ import $ from 'helpers/jquery'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import { mount, createLocalVue } from '@vue/test-utils'; import NotesApp from '~/notes/components/notes_app.vue'; @@ -9,19 +11,10 @@ import { setTestTimeout } from 'helpers/timeout'; // TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491) import * as mockData from '../../../javascripts/notes/mock_data'; -const originalInterceptors = [...Vue.http.interceptors]; - -const emptyResponseInterceptor = (request, next) => { - next( - request.respondWith(JSON.stringify([]), { - status: 200, - }), - ); -}; - setTestTimeout(1000); describe('note_app', () => { + let axiosMock; let mountComponent; let wrapper; let store; @@ -45,6 +38,8 @@ describe('note_app', () => { beforeEach(() => { $('body').attr('data-page', 'projects:merge_requests:show'); + axiosMock = new AxiosMockAdapter(axios); + store = createStore(); mountComponent = data => { const propsData = data || { @@ -74,12 +69,12 @@ describe('note_app', () => { afterEach(() => { wrapper.destroy(); - Vue.http.interceptors = [...originalInterceptors]; + axiosMock.restore(); }); describe('set data', () => { beforeEach(() => { - Vue.http.interceptors.push(emptyResponseInterceptor); + axiosMock.onAny().reply(200, []); wrapper = mountComponent(); return waitForDiscussionsRequest(); }); @@ -105,7 +100,7 @@ describe('note_app', () => { beforeEach(() => { setFixtures('<div class="js-discussions-count"></div>'); - Vue.http.interceptors.push(mockData.individualNoteInterceptor); + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); wrapper = mountComponent(); return waitForDiscussionsRequest(); }); @@ -146,7 +141,7 @@ describe('note_app', () => { beforeEach(() => { setFixtures('<div class="js-discussions-count"></div>'); - Vue.http.interceptors.push(mockData.individualNoteInterceptor); + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); store.state.commentsDisabled = true; wrapper = mountComponent(); return waitForDiscussionsRequest(); @@ -163,7 +158,7 @@ describe('note_app', () => { describe('while fetching data', () => { beforeEach(() => { - Vue.http.interceptors.push(emptyResponseInterceptor); + axiosMock.onAny().reply(200, []); wrapper = mountComponent(); }); @@ -184,7 +179,7 @@ describe('note_app', () => { describe('update note', () => { describe('individual note', () => { beforeEach(() => { - Vue.http.interceptors.push(mockData.individualNoteInterceptor); + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); jest.spyOn(service, 'updateNote'); wrapper = mountComponent(); return waitForDiscussionsRequest().then(() => { @@ -206,7 +201,7 @@ describe('note_app', () => { describe('discussion note', () => { beforeEach(() => { - Vue.http.interceptors.push(mockData.discussionNoteInterceptor); + axiosMock.onAny().reply(mockData.getDiscussionNoteResponse); jest.spyOn(service, 'updateNote'); wrapper = mountComponent(); return waitForDiscussionsRequest().then(() => { @@ -229,7 +224,7 @@ describe('note_app', () => { describe('new note form', () => { beforeEach(() => { - Vue.http.interceptors.push(mockData.individualNoteInterceptor); + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); wrapper = mountComponent(); return waitForDiscussionsRequest(); }); @@ -259,7 +254,7 @@ describe('note_app', () => { describe('edit form', () => { beforeEach(() => { - Vue.http.interceptors.push(mockData.individualNoteInterceptor); + axiosMock.onAny().reply(mockData.getIndividualNoteResponse); wrapper = mountComponent(); return waitForDiscussionsRequest(); }); @@ -287,7 +282,7 @@ describe('note_app', () => { describe('emoji awards', () => { beforeEach(() => { - Vue.http.interceptors.push(emptyResponseInterceptor); + axiosMock.onAny().reply(200, []); wrapper = mountComponent(); return waitForDiscussionsRequest(); }); diff --git a/spec/frontend/releases/components/milestone_list_spec.js b/spec/frontend/releases/components/milestone_list_spec.js new file mode 100644 index 00000000000..f267177ddab --- /dev/null +++ b/spec/frontend/releases/components/milestone_list_spec.js @@ -0,0 +1,56 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; +import MilestoneList from '~/releases/components/milestone_list.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import _ from 'underscore'; +import { milestones } from '../mock_data'; + +describe('Milestone list', () => { + let wrapper; + + const factory = milestonesProp => { + wrapper = shallowMount(MilestoneList, { + propsData: { + milestones: milestonesProp, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the milestone icon', () => { + factory(milestones); + + expect(wrapper.find(Icon).exists()).toBe(true); + }); + + it('renders the label as "Milestone" if only a single milestone is passed in', () => { + factory(milestones.slice(0, 1)); + + expect(wrapper.find('.js-label-text').text()).toEqual('Milestone'); + }); + + it('renders the label as "Milestones" if more than one milestone is passed in', () => { + factory(milestones); + + expect(wrapper.find('.js-label-text').text()).toEqual('Milestones'); + }); + + it('renders a link to the milestone with a tooltip', () => { + const milestone = _.first(milestones); + factory([milestone]); + + const milestoneLink = wrapper.find(GlLink); + + expect(milestoneLink.exists()).toBe(true); + + expect(milestoneLink.text()).toBe(milestone.title); + + expect(milestoneLink.attributes('href')).toBe(milestone.web_url); + + expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description); + }); +}); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js new file mode 100644 index 00000000000..4be5d500fd9 --- /dev/null +++ b/spec/frontend/releases/components/release_block_spec.js @@ -0,0 +1,120 @@ +import { mount } from '@vue/test-utils'; +import ReleaseBlock from '~/releases/components/release_block.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { first } from 'underscore'; +import { release } from '../mock_data'; + +describe('Release block', () => { + let wrapper; + + const factory = releaseProp => { + wrapper = mount(ReleaseBlock, { + propsData: { + release: releaseProp, + }, + sync: false, + }); + }; + + const milestoneListExists = () => wrapper.find('.js-milestone-list').exists(); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with default props', () => { + beforeEach(() => { + factory(release); + }); + + it("renders the block with an id equal to the release's tag name", () => { + expect(wrapper.attributes().id).toBe('v0.3'); + }); + + it('renders release name', () => { + expect(wrapper.text()).toContain(release.name); + }); + + it('renders commit sha', () => { + expect(wrapper.text()).toContain(release.commit.short_id); + }); + + it('renders tag name', () => { + expect(wrapper.text()).toContain(release.tag_name); + }); + + it('renders release date', () => { + expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at)); + }); + + it('renders number of assets provided', () => { + expect(wrapper.find('.js-assets-count').text()).toContain(release.assets.count); + }); + + it('renders dropdown with the sources', () => { + expect(wrapper.findAll('.js-sources-dropdown li').length).toEqual( + release.assets.sources.length, + ); + + expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual( + first(release.assets.sources).url, + ); + + expect(wrapper.find('.js-sources-dropdown li a').text()).toContain( + first(release.assets.sources).format, + ); + }); + + it('renders list with the links provided', () => { + expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length); + + expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual( + first(release.assets.links).url, + ); + + expect(wrapper.find('.js-assets-list li a').text()).toContain( + first(release.assets.links).name, + ); + }); + + it('renders author avatar', () => { + expect(wrapper.find('.user-avatar-link').exists()).toBe(true); + }); + + describe('external label', () => { + it('renders external label when link is external', () => { + expect(wrapper.find('.js-assets-list li a').text()).toContain('external source'); + }); + + it('does not render external label when link is not external', () => { + expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain( + 'external source', + ); + }); + }); + + it('renders the milestone list if at least one milestone is associated to the release', () => { + factory(release); + + expect(milestoneListExists()).toBe(true); + }); + }); + + it('does not render the milestone list if no milestones are associated to the release', () => { + const releaseClone = JSON.parse(JSON.stringify(release)); + delete releaseClone.milestone; + + factory(releaseClone); + + expect(milestoneListExists()).toBe(false); + }); + + it('renders upcoming release badge', () => { + const releaseClone = JSON.parse(JSON.stringify(release)); + releaseClone.upcoming_release = true; + + factory(releaseClone); + + expect(wrapper.text()).toContain('Upcoming Release'); + }); +}); diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js new file mode 100644 index 00000000000..a0885813c7e --- /dev/null +++ b/spec/frontend/releases/mock_data.js @@ -0,0 +1,97 @@ +export const milestones = [ + { + id: 50, + iid: 2, + project_id: 18, + title: '13.6', + description: 'The 13.6 milestone!', + state: 'active', + created_at: '2019-08-27T17:22:38.280Z', + updated_at: '2019-08-27T17:22:38.280Z', + due_date: '2019-09-19', + start_date: '2019-08-31', + web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/2', + }, + { + id: 49, + iid: 1, + project_id: 18, + title: '13.5', + description: 'The 13.5 milestone!', + state: 'active', + created_at: '2019-08-26T17:55:48.643Z', + updated_at: '2019-08-26T17:55:48.643Z', + due_date: '2019-10-11', + start_date: '2019-08-19', + web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/1', + }, +]; + +export const release = { + name: 'New release', + tag_name: 'v0.3', + description: 'A super nice release!', + description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>', + created_at: '2019-08-26T17:54:04.952Z', + released_at: '2019-08-26T17:54:04.807Z', + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://0.0.0.0:3001/root', + }, + commit: { + id: 'c22b0728d1b465f82898c884d32b01aa642f96c1', + short_id: 'c22b0728', + created_at: '2019-08-26T17:47:07.000Z', + parent_ids: [], + title: 'Initial commit', + message: 'Initial commit', + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2019-08-26T17:47:07.000Z', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2019-08-26T17:47:07.000Z', + }, + upcoming_release: false, + milestone: milestones[0], + assets: { + count: 5, + sources: [ + { + format: 'zip', + url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip', + }, + { + format: 'tar.gz', + url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz', + }, + { + format: 'tar.bz2', + url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2', + }, + { + format: 'tar', + url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar', + }, + ], + links: [ + { + id: 1, + name: 'my link', + url: 'https://google.com', + external: true, + }, + { + id: 2, + name: 'my second link', + url: + 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', + external: false, + }, + ], + }, +}; diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index f0e58cbda4d..98a9150d05d 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -647,24 +647,12 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { }, }; -export function individualNoteInterceptor(request, next) { - const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; - - next( - request.respondWith(JSON.stringify(body), { - status: 200, - }), - ); +export function getIndividualNoteResponse(config) { + return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]]; } -export function discussionNoteInterceptor(request, next) { - const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; - - next( - request.respondWith(JSON.stringify(body), { - status: 200, - }), - ); +export function getDiscussionNoteResponse(config) { + return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]]; } export const notesWithDescriptionChanges = [ diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 1fd4a9a7612..e3cc025cf49 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,9 +1,6 @@ -import Vue from 'vue'; import $ from 'jquery'; -import _ from 'underscore'; import Api from '~/api'; import { TEST_HOST } from 'spec/test_constants'; -import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import actionsModule, * as actions from '~/notes/stores/actions'; import * as mutationTypes from '~/notes/stores/mutation_types'; import * as notesConstants from '~/notes/constants'; @@ -29,6 +26,7 @@ describe('Actions Notes Store', () => { let state; let store; let flashSpy; + let axiosMock; beforeEach(() => { store = createStore(); @@ -36,10 +34,12 @@ describe('Actions Notes Store', () => { dispatch = jasmine.createSpy('dispatch'); state = {}; flashSpy = spyOnDependency(actionsModule, 'Flash'); + axiosMock = new AxiosMockAdapter(axios); }); afterEach(() => { resetStore(store); + axiosMock.restore(); }); describe('setNotesData', () => { @@ -160,20 +160,8 @@ describe('Actions Notes Store', () => { }); describe('async methods', () => { - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify({}), { - status: 200, - }), - ); - }; - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + axiosMock.onAny().reply(200, {}); }); describe('closeIssue', () => { @@ -259,7 +247,7 @@ describe('Actions Notes Store', () => { beforeEach(done => { jasmine.clock().install(); - spyOn(Vue.http, 'get').and.callThrough(); + spyOn(axios, 'get').and.callThrough(); store .dispatch('setNotesData', notesDataMock) @@ -272,31 +260,15 @@ describe('Actions Notes Store', () => { }); it('calls service with last fetched state', done => { - const interceptor = (request, next) => { - next( - request.respondWith( - JSON.stringify({ - notes: [], - last_fetched_at: '123456', - }), - { - status: 200, - headers: { - 'poll-interval': '1000', - }, - }, - ), - ); - }; - - Vue.http.interceptors.push(interceptor); - Vue.http.interceptors.push(headersInterceptor); + axiosMock + .onAny() + .reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' }); store .dispatch('poll') .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { - expect(Vue.http.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalled(); expect(store.state.lastFetchedAt).toBe('123456'); jasmine.clock().tick(1500); @@ -308,16 +280,12 @@ describe('Actions Notes Store', () => { }), ) .then(() => { - expect(Vue.http.get.calls.count()).toBe(2); - expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({ + expect(axios.get.calls.count()).toBe(2); + expect(axios.get.calls.mostRecent().args[1].headers).toEqual({ 'X-Last-Fetched-At': '123456', }); }) .then(() => store.dispatch('stopPolling')) - .then(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor); - }) .then(done) .catch(done.fail); }); @@ -338,10 +306,8 @@ describe('Actions Notes Store', () => { describe('removeNote', () => { const endpoint = `${TEST_HOST}/note`; - let axiosMock; beforeEach(() => { - axiosMock = new AxiosMockAdapter(axios); axiosMock.onDelete(endpoint).replyOnce(200, {}); $('body').attr('data-page', ''); @@ -411,10 +377,8 @@ describe('Actions Notes Store', () => { describe('deleteNote', () => { const endpoint = `${TEST_HOST}/note`; - let axiosMock; beforeEach(() => { - axiosMock = new AxiosMockAdapter(axios); axiosMock.onDelete(endpoint).replyOnce(200, {}); $('body').attr('data-page', ''); @@ -454,20 +418,9 @@ describe('Actions Notes Store', () => { id: 1, valid: true, }; - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(res), { - status: 200, - }), - ); - }; beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + axiosMock.onAny().reply(200, res); }); it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', done => { @@ -501,20 +454,9 @@ describe('Actions Notes Store', () => { const res = { errors: ['error'], }; - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(res), { - status: 200, - }), - ); - }; beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + axiosMock.onAny().replyOnce(200, res); }); it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', done => { @@ -534,20 +476,9 @@ describe('Actions Notes Store', () => { const res = { resolved: true, }; - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(res), { - status: 200, - }), - ); - }; beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + axiosMock.onAny().reply(200, res); }); describe('as note', () => { @@ -720,32 +651,19 @@ describe('Actions Notes Store', () => { }); describe('replyToDiscussion', () => { - let res = { discussion: { notes: [] } }; const payload = { endpoint: TEST_HOST, data: {} }; - const interceptor = (request, next) => { - next( - request.respondWith(JSON.stringify(res), { - status: 200, - }), - ); - }; - - beforeEach(() => { - Vue.http.interceptors.push(interceptor); - }); - - afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); - }); it('updates discussion if response contains disussion', done => { + const discussion = { notes: [] }; + axiosMock.onAny().reply(200, { discussion }); + testAction( actions.replyToDiscussion, payload, { notesById: {}, }, - [{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }], + [{ type: mutationTypes.UPDATE_DISCUSSION, payload: discussion }], [ { type: 'updateMergeRequestWidget' }, { type: 'startTaskList' }, @@ -756,7 +674,8 @@ describe('Actions Notes Store', () => { }); it('adds a reply to a discussion', done => { - res = {}; + const res = {}; + axiosMock.onAny().reply(200, res); testAction( actions.replyToDiscussion, diff --git a/spec/javascripts/releases/components/release_block_spec.js b/spec/javascripts/releases/components/release_block_spec.js deleted file mode 100644 index fdf23f3f69d..00000000000 --- a/spec/javascripts/releases/components/release_block_spec.js +++ /dev/null @@ -1,168 +0,0 @@ -import Vue from 'vue'; -import component from '~/releases/components/release_block.vue'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; - -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Release block', () => { - const Component = Vue.extend(component); - - const release = { - name: 'Bionic Beaver', - tag_name: '18.04', - description: '## changelog\n\n* line 1\n* line2', - description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>', - author_name: 'Release bot', - author_email: 'release-bot@example.com', - released_at: '2012-05-28T05:00:00-07:00', - author: { - avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png', - id: 482476, - name: 'John Doe', - path: '/johndoe', - state: 'active', - status_tooltip_html: null, - username: 'johndoe', - web_url: 'https://gitlab.com/johndoe', - }, - commit: { - id: '2695effb5807a22ff3d138d593fd856244e155e7', - short_id: '2695effb', - title: 'Initial commit', - created_at: '2017-07-26T11:08:53.000+02:00', - parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'], - message: 'Initial commit', - author_name: 'John Smith', - author_email: 'john@example.com', - authored_date: '2012-05-28T04:42:42-07:00', - committer_name: 'Jack Smith', - committer_email: 'jack@example.com', - committed_date: '2012-05-28T04:42:42-07:00', - }, - assets: { - count: 6, - sources: [ - { - format: 'zip', - url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip', - }, - { - format: 'tar.gz', - url: - 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz', - }, - { - format: 'tar.bz2', - url: - 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2', - }, - { - format: 'tar', - url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar', - }, - ], - links: [ - { - name: 'release-18.04.dmg', - url: 'https://my-external-hosting.example.com/scrambled-url/', - external: true, - }, - { - name: 'binary-linux-amd64', - url: - 'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50', - external: false, - }, - ], - }, - }; - let vm; - - const factory = props => mountComponent(Component, { release: props }); - - beforeEach(() => { - vm = factory(release); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it("renders the block with an id equal to the release's tag name", () => { - expect(vm.$el.id).toBe('18.04'); - }); - - it('renders release name', () => { - expect(vm.$el.textContent).toContain(release.name); - }); - - it('renders commit sha', () => { - expect(vm.$el.textContent).toContain(release.commit.short_id); - }); - - it('renders tag name', () => { - expect(vm.$el.textContent).toContain(release.tag_name); - }); - - it('renders release date', () => { - expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at)); - }); - - it('renders number of assets provided', () => { - expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count); - }); - - it('renders dropdown with the sources', () => { - expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual( - release.assets.sources.length, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual( - release.assets.sources[0].url, - ); - - expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain( - release.assets.sources[0].format, - ); - }); - - it('renders list with the links provided', () => { - expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual( - release.assets.links.length, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual( - release.assets.links[0].url, - ); - - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain( - release.assets.links[0].name, - ); - }); - - it('renders author avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); - }); - - describe('external label', () => { - it('renders external label when link is external', () => { - expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source'); - }); - - it('does not render external label when link is not external', () => { - expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain( - 'external source', - ); - }); - }); - - describe('with upcoming_release flag', () => { - beforeEach(() => { - vm = factory(Object.assign({}, release, { upcoming_release: true })); - }); - - it('renders upcoming release badge', () => { - expect(vm.$el.textContent).toContain('Upcoming Release'); - }); - }); -}); diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb new file mode 100644 index 00000000000..baa6d774864 --- /dev/null +++ b/spec/services/issues/zoom_link_service_spec.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Issues::ZoomLinkService do + set(:user) { create(:user) } + set(:issue) { create(:issue) } + + let(:project) { issue.project } + let(:service) { described_class.new(issue, user) } + let(:zoom_link) { 'https://zoom.us/j/123456789' } + + before do + project.add_reporter(user) + end + + shared_context 'with Zoom link' do + before do + issue.update!(description: "Description\n\n#{zoom_link}") + end + end + + shared_context 'with Zoom link not at the end' do + before do + issue.update!(description: "Description with #{zoom_link} some where") + end + end + + shared_context 'without Zoom link' do + before do + issue.update!(description: "Description\n\nhttp://example.com") + end + end + + shared_context 'without issue description' do + before do + issue.update!(description: nil) + end + end + + shared_context 'feature flag disabled' do + before do + stub_feature_flags(issue_zoom_integration: false) + end + end + + shared_context 'insufficient permissions' do + before do + project.add_guest(user) + end + end + + describe '#add_link' do + shared_examples 'can add link' do + it 'appends the link to issue description' do + expect(result).to be_success + expect(result.payload[:description]) + .to eq("#{issue.description}\n\n#{zoom_link}") + end + end + + shared_examples 'cannot add link' do + it 'cannot add the link' do + expect(result).to be_error + expect(result.message).to eq('Failed to add a Zoom meeting') + end + end + + subject(:result) { service.add_link(zoom_link) } + + context 'without Zoom link in the issue description' do + include_context 'without Zoom link' + include_examples 'can add link' + + context 'with invalid Zoom link' do + let(:zoom_link) { 'https://not-zoom.link' } + + include_examples 'cannot add link' + end + + context 'when feature flag is disabled' do + include_context 'feature flag disabled' + include_examples 'cannot add link' + end + + context 'with insufficient permissions' do + include_context 'insufficient permissions' + include_examples 'cannot add link' + end + end + + context 'with Zoom link in the issue description' do + include_context 'with Zoom link' + include_examples 'cannot add link' + + context 'but not at the end' do + include_context 'with Zoom link not at the end' + include_examples 'can add link' + end + end + + context 'without issue description' do + include_context 'without issue description' + include_examples 'can add link' + end + end + + describe '#can_add_link?' do + subject { service.can_add_link? } + + context 'without Zoom link in the issue description' do + include_context 'without Zoom link' + + it { is_expected.to eq(true) } + + context 'when feature flag is disabled' do + include_context 'feature flag disabled' + + it { is_expected.to eq(false) } + end + + context 'with insufficient permissions' do + include_context 'insufficient permissions' + + it { is_expected.to eq(false) } + end + end + + context 'with Zoom link in the issue description' do + include_context 'with Zoom link' + + it { is_expected.to eq(false) } + end + end + + describe '#remove_link' do + shared_examples 'cannot remove link' do + it 'cannot remove the link' do + expect(result).to be_error + expect(result.message).to eq('Failed to remove a Zoom meeting') + end + end + + subject(:result) { service.remove_link } + + context 'with Zoom link in the issue description' do + include_context 'with Zoom link' + + it 'removes the link from the issue description' do + expect(result).to be_success + expect(result.payload[:description]) + .to eq(issue.description.delete_suffix("\n\n#{zoom_link}")) + end + + context 'when feature flag is disabled' do + include_context 'feature flag disabled' + include_examples 'cannot remove link' + end + + context 'with insufficient permissions' do + include_context 'insufficient permissions' + include_examples 'cannot remove link' + end + + context 'but not at the end' do + include_context 'with Zoom link not at the end' + include_examples 'cannot remove link' + end + end + + context 'without Zoom link in the issue description' do + include_context 'without Zoom link' + include_examples 'cannot remove link' + end + + context 'without issue description' do + include_context 'without issue description' + include_examples 'cannot remove link' + end + end + + describe '#can_remove_link?' do + subject { service.can_remove_link? } + + context 'with Zoom link in the issue description' do + include_context 'with Zoom link' + + it { is_expected.to eq(true) } + + context 'when feature flag is disabled' do + include_context 'feature flag disabled' + + it { is_expected.to eq(false) } + end + + context 'with insufficient permissions' do + include_context 'insufficient permissions' + + it { is_expected.to eq(false) } + end + end + + context 'without Zoom link in the issue description' do + include_context 'without Zoom link' + + it { is_expected.to eq(false) } + end + end + + describe '#parse_link' do + subject { service.parse_link(description) } + + context 'with valid Zoom links' do + where(:description) do + [ + 'Some text https://zoom.us/j/123456789 more text', + 'Mixed https://zoom.us/j/123456789 http://example.com', + 'Multiple link https://zoom.us/my/name https://zoom.us/j/123456789' + ] + end + + with_them do + it { is_expected.to eq('https://zoom.us/j/123456789') } + end + end + + context 'with invalid Zoom links' do + where(:description) do + [ + nil, + '', + 'Text only', + 'Non-Zoom http://example.com', + 'Almost Zoom http://zoom.us' + ] + end + + with_them do + it { is_expected.to eq(nil) } + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb new file mode 100644 index 00000000000..cb5460bde23 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +shared_examples 'zoom quick actions' do + let(:zoom_link) { 'https://zoom.us/j/123456789' } + let(:invalid_zoom_link) { 'https://invalid-zoom' } + + before do + issue.update!(description: description) + end + + describe '/zoom' do + shared_examples 'skip silently' do + it 'skip addition silently' do + add_note("/zoom #{zoom_link}") + + wait_for_requests + + expect(page).not_to have_content('Zoom meeting added') + expect(page).not_to have_content('Failed to add a Zoom meeting') + expect(issue.reload.description).to eq(description) + end + end + + shared_examples 'success' do + it 'adds a Zoom link' do + add_note("/zoom #{zoom_link}") + + wait_for_requests + + expect(page).to have_content('Zoom meeting added') + expect(issue.reload.description).to end_with(zoom_link) + end + end + + context 'without issue description' do + let(:description) { nil } + + include_examples 'success' + + it 'cannot add invalid zoom link' do + add_note("/zoom #{invalid_zoom_link}") + + wait_for_requests + + expect(page).to have_content('Failed to add a Zoom meeting') + expect(page).not_to have_content(zoom_link) + end + + context 'when feature flag disabled' do + before do + stub_feature_flags(issue_zoom_integration: false) + end + + include_examples 'skip silently' + end + end + + context 'with Zoom link not at the end of the issue description' do + let(:description) { "A link #{zoom_link} not at the end" } + + include_examples 'success' + end + + context 'with Zoom link at end of the issue description' do + let(:description) { "Text\n#{zoom_link}" } + + include_examples 'skip silently' + end + end + + describe '/remove_zoom' do + shared_examples 'skip silently' do + it 'skip removal silently' do + add_note('/remove_zoom') + + wait_for_requests + + expect(page).not_to have_content('Zoom meeting removed') + expect(page).not_to have_content('Failed to remove a Zoom meeting') + expect(issue.reload.description).to eq(description) + end + end + + context 'with Zoom link in the description' do + let(:description) { "Text with #{zoom_link}\n\n\n#{zoom_link}" } + + it 'removes last Zoom link' do + add_note('/remove_zoom') + + wait_for_requests + + expect(page).to have_content('Zoom meeting removed') + expect(issue.reload.description).to eq("Text with #{zoom_link}") + end + + context 'when feature flag disabled' do + before do + stub_feature_flags(issue_zoom_integration: false) + end + + include_examples 'skip silently' + end + end + + context 'with a Zoom link not at the end of the description' do + let(:description) { "A link #{zoom_link} not at the end" } + + include_examples 'skip silently' + end + end +end |