Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-09-14 03:06:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-09-14 03:06:25 +0300
commita93dfc1b7e55b118b1cf4a67afeb46556292914c (patch)
tree65b874b7940d0d05c4ebedaef43b8a1009362651 /spec
parent188a57f93bba5953800de490fcc6246966a073fd (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb1
-rw-r--r--spec/frontend/notes/components/note_app_spec.js35
-rw-r--r--spec/frontend/releases/components/milestone_list_spec.js56
-rw-r--r--spec/frontend/releases/components/release_block_spec.js120
-rw-r--r--spec/frontend/releases/mock_data.js97
-rw-r--r--spec/javascripts/notes/mock_data.js20
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js121
-rw-r--r--spec/javascripts/releases/components/release_block_spec.js168
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb243
-rw-r--r--spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb111
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