summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 21:09:23 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-25 21:09:23 +0000
commit32fd4cd5e2134511936899d6bcc4aaf18b9be6fd (patch)
tree10378ceffed52dd0e160a0d9bcf3c5ab72c18958 /spec
parent951616a26a61e880860ad862c1d45a8e3762b4bc (diff)
downloadgitlab-ce-32fd4cd5e2134511936899d6bcc4aaf18b9be6fd.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/explore/snippets_controller_spec.rb31
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js (renamed from spec/javascripts/badges/components/badge_form_spec.js)27
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js (renamed from spec/javascripts/badges/components/badge_list_row_spec.js)23
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js (renamed from spec/javascripts/badges/components/badge_list_spec.js)10
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js (renamed from spec/javascripts/badges/components/badge_settings_spec.js)27
-rw-r--r--spec/frontend/badges/components/badge_spec.js (renamed from spec/javascripts/badges/components/badge_spec.js)14
-rw-r--r--spec/frontend/badges/dummy_badge.js (renamed from spec/javascripts/badges/dummy_badge.js)0
-rw-r--r--spec/frontend/badges/store/actions_spec.js (renamed from spec/javascripts/badges/store/actions_spec.js)94
-rw-r--r--spec/frontend/badges/store/mutations_spec.js (renamed from spec/javascripts/badges/store/mutations_spec.js)0
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js91
-rw-r--r--spec/frontend/notes/components/discussion_jump_to_next_button_spec.js12
-rw-r--r--spec/frontend/notes/components/discussion_keyboard_navigator_spec.js73
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js178
-rw-r--r--spec/javascripts/notes/components/discussion_counter_spec.js90
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb35
-rw-r--r--spec/lib/gitlab/tracing_spec.rb69
-rw-r--r--spec/models/ci/build_spec.rb58
-rw-r--r--spec/models/environment_status_spec.rb16
-rw-r--r--spec/models/pages_domain_spec.rb8
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb23
-rw-r--r--spec/requests/api/internal/pages_spec.rb14
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb50
23 files changed, 595 insertions, 349 deletions
diff --git a/spec/controllers/explore/snippets_controller_spec.rb b/spec/controllers/explore/snippets_controller_spec.rb
index fa659c6df7f..ab91faa6cef 100644
--- a/spec/controllers/explore/snippets_controller_spec.rb
+++ b/spec/controllers/explore/snippets_controller_spec.rb
@@ -4,12 +4,33 @@ require 'spec_helper'
describe Explore::SnippetsController do
describe 'GET #index' do
- it_behaves_like 'paginated collection' do
- let(:collection) { Snippet.all }
+ let!(:project_snippet) { create_list(:project_snippet, 3, :public) }
+ let!(:personal_snippet) { create_list(:personal_snippet, 3, :public) }
- before do
- create(:personal_snippet, :public)
- end
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
+ end
+
+ it 'renders' do
+ get :index
+
+ snippets = assigns(:snippets)
+
+ expect(snippets).to be_a(::Kaminari::PaginatableWithoutCount)
+ expect(snippets.size).to eq(2)
+ expect(snippets).to all(be_a(PersonalSnippet))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'renders pagination' do
+ get :index, params: { page: 2 }
+
+ snippets = assigns(:snippets)
+
+ expect(snippets).to be_a(::Kaminari::PaginatableWithoutCount)
+ expect(snippets.size).to eq(1)
+ expect(assigns(:snippets)).to all(be_a(PersonalSnippet))
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/javascripts/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index c7aa7fa63b1..d61bd29ca9d 100644
--- a/spec/javascripts/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import store from '~/badges/store';
import createEmptyBadge from '~/badges/empty_badge';
import BadgeForm from '~/badges/components/badge_form.vue';
-import { DUMMY_IMAGE_URL, TEST_HOST } from '../../test_constants';
+import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
// avoid preview background process
BadgeForm.methods.debouncedPreview = () => {};
@@ -41,7 +41,7 @@ describe('BadgeForm component', () => {
describe('onCancel', () => {
it('calls stopEditing', () => {
- spyOn(vm, 'stopEditing');
+ jest.spyOn(vm, 'stopEditing').mockImplementation(() => {});
vm.onCancel();
@@ -68,14 +68,14 @@ describe('BadgeForm component', () => {
const expectInvalidInput = inputElementSelector => {
const inputElement = vm.$el.querySelector(inputElementSelector);
- expect(inputElement).toBeMatchedBy(':invalid');
+ expect(inputElement.checkValidity()).toBe(false);
const feedbackElement = vm.$el.querySelector(`${inputElementSelector} + .invalid-feedback`);
expect(feedbackElement).toBeVisible();
};
- beforeEach(() => {
- spyOn(vm, submitAction).and.returnValue(Promise.resolve());
+ beforeEach(done => {
+ jest.spyOn(vm, submitAction).mockReturnValue(Promise.resolve());
store.replaceState({
...store.state,
badgeInAddForm: createEmptyBadge(),
@@ -83,9 +83,14 @@ describe('BadgeForm component', () => {
isSaving: false,
});
- setValue(nameSelector, 'TestBadge');
- setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
- setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
+ Vue.nextTick()
+ .then(() => {
+ setValue(nameSelector, 'TestBadge');
+ setValue(linkUrlSelector, `${TEST_HOST}/link/url`);
+ setValue(imageUrlSelector, `${window.location.origin}${DUMMY_IMAGE_URL}`);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('returns immediately if imageUrl is empty', () => {
@@ -131,8 +136,8 @@ describe('BadgeForm component', () => {
it(`calls ${submitAction}`, () => {
submitForm();
- expect(findImageUrlElement()).toBeMatchedBy(':valid');
- expect(findLinkUrlElement()).toBeMatchedBy(':valid');
+ expect(findImageUrlElement().checkValidity()).toBe(true);
+ expect(findLinkUrlElement().checkValidity()).toBe(true);
expect(vm[submitAction]).toHaveBeenCalled();
});
};
diff --git a/spec/javascripts/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index d1434737085..31f0d850857 100644
--- a/spec/javascripts/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import store from '~/badges/store';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
@@ -40,15 +39,15 @@ describe('BadgeListRow component', () => {
});
it('renders the badge name', () => {
- expect(vm.$el).toContainText(badge.name);
+ expect(vm.$el.innerText).toMatch(badge.name);
});
it('renders the badge link', () => {
- expect(vm.$el).toContainText(badge.linkUrl);
+ expect(vm.$el.innerText).toMatch(badge.linkUrl);
});
it('renders the badge kind', () => {
- expect(vm.$el).toContainText('Project Badge');
+ expect(vm.$el.innerText).toMatch('Project Badge');
});
it('shows edit and delete buttons', () => {
@@ -66,7 +65,7 @@ describe('BadgeListRow component', () => {
});
it('calls editBadge when clicking then edit button', () => {
- spyOn(vm, 'editBadge');
+ jest.spyOn(vm, 'editBadge').mockImplementation(() => {});
const editButton = vm.$el.querySelector('.table-button-footer button:first-of-type');
editButton.click();
@@ -75,13 +74,17 @@ describe('BadgeListRow component', () => {
});
it('calls updateBadgeInModal and shows modal when clicking then delete button', done => {
- spyOn(vm, 'updateBadgeInModal');
- $('#delete-badge-modal').on('shown.bs.modal', () => done());
+ jest.spyOn(vm, 'updateBadgeInModal').mockImplementation(() => {});
const deleteButton = vm.$el.querySelector('.table-button-footer button:last-of-type');
deleteButton.click();
- expect(vm.updateBadgeInModal).toHaveBeenCalled();
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.updateBadgeInModal).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
describe('for a group badge', () => {
@@ -94,7 +97,7 @@ describe('BadgeListRow component', () => {
});
it('renders the badge kind', () => {
- expect(vm.$el).toContainText('Group Badge');
+ expect(vm.$el.innerText).toMatch('Group Badge');
});
it('hides edit and delete buttons', () => {
diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 3af194454e3..5ffc046eb97 100644
--- a/spec/javascripts/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import store from '~/badges/store';
import BadgeList from '~/badges/components/badge_list.vue';
@@ -22,6 +22,10 @@ describe('BadgeList component', () => {
kind: PROJECT_BADGE,
isLoading: false,
});
+
+ // Can be removed once GlLoadingIcon no longer throws a warning
+ jest.spyOn(global.console, 'warn').mockImplementation(() => jest.fn());
+
vm = mountComponentWithStore(Component, {
el: '#dummy-element',
store,
@@ -49,7 +53,7 @@ describe('BadgeList component', () => {
Vue.nextTick()
.then(() => {
- expect(vm.$el).toContainText('This project has no badges');
+ expect(vm.$el.innerText).toMatch('This project has no badges');
})
.then(done)
.catch(done.fail);
@@ -82,7 +86,7 @@ describe('BadgeList component', () => {
Vue.nextTick()
.then(() => {
- expect(vm.$el).toContainText('This group has no badges');
+ expect(vm.$el.innerText).toMatch('This group has no badges');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 479a905661b..8c3f1ea2749 100644
--- a/spec/javascripts/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import store from '~/badges/store';
import BadgeSettings from '~/badges/components/badge_settings.vue';
import { createDummyBadge } from '../dummy_badge';
@@ -19,6 +18,10 @@ describe('BadgeSettings component', () => {
data-target="#delete-badge-modal"
>Show modal</button>
`);
+
+ // Can be removed once GlLoadingIcon no longer throws a warning
+ jest.spyOn(global.console, 'warn').mockImplementation(() => jest.fn());
+
vm = mountComponentWithStore(Component, {
el: '#dummy-element',
store,
@@ -35,20 +38,16 @@ describe('BadgeSettings component', () => {
const modal = vm.$el.querySelector('#delete-badge-modal');
const button = document.getElementById('dummy-modal-button');
- $(modal).on('shown.bs.modal', () => {
- expect(modal).toContainText('Delete badge?');
- const badgeElement = modal.querySelector('img.project-badge');
-
- expect(badgeElement).not.toBe(null);
- expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
-
- done();
- });
+ button.click();
Vue.nextTick()
.then(() => {
- button.click();
+ expect(modal.innerText).toMatch('Delete badge?');
+ const badgeElement = modal.querySelector('img.project-badge');
+ expect(badgeElement).not.toBe(null);
+ expect(badgeElement.getAttribute('src')).toBe(badge.renderedImageUrl);
})
+ .then(done)
.catch(done.fail);
});
@@ -67,7 +66,7 @@ describe('BadgeSettings component', () => {
expect(badgeListElement).not.toBe(null);
expect(badgeListElement).toBeVisible();
- expect(badgeListElement).toContainText('Your badges');
+ expect(badgeListElement.innerText).toMatch('Your badges');
});
describe('when editing', () => {
@@ -103,7 +102,7 @@ describe('BadgeSettings component', () => {
describe('methods', () => {
describe('onSubmitModal', () => {
it('triggers ', () => {
- spyOn(vm, 'deleteBadge').and.callFake(() => Promise.resolve());
+ jest.spyOn(vm, 'deleteBadge').mockImplementation(() => Promise.resolve());
const modal = vm.$el.querySelector('#delete-badge-modal');
const deleteButton = modal.querySelector('.btn-danger');
diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index 14490b1bbd1..43004004fb2 100644
--- a/spec/javascripts/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import { DUMMY_IMAGE_URL, TEST_HOST } from 'spec/test_constants';
import Badge from '~/badges/components/badge.vue';
@@ -23,9 +23,11 @@ describe('Badge component', () => {
const createComponent = (props, el = null) => {
vm = mountComponent(Component, props, el);
const { badgeImage } = findElements();
- return new Promise(resolve => badgeImage.addEventListener('load', resolve)).then(() =>
- Vue.nextTick(),
- );
+ return new Promise(resolve => {
+ badgeImage.addEventListener('load', resolve);
+ // Manually dispatch load event as it is not triggered
+ badgeImage.dispatchEvent(new Event('load'));
+ }).then(() => Vue.nextTick());
};
afterEach(() => {
@@ -111,7 +113,7 @@ describe('Badge component', () => {
expect(badgeImage).toBeVisible();
expect(loadingIcon).toBeHidden();
expect(reloadButton).toBeHidden();
- expect(vm.$el.innerText).toBe('');
+ expect(vm.$el.querySelector('.btn-group')).toBeHidden();
});
it('shows a loading icon when loading', done => {
@@ -124,7 +126,7 @@ describe('Badge component', () => {
expect(badgeImage).toBeHidden();
expect(loadingIcon).toBeVisible();
expect(reloadButton).toBeHidden();
- expect(vm.$el.innerText).toBe('');
+ expect(vm.$el.querySelector('.btn-group')).toBeHidden();
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/badges/dummy_badge.js b/spec/frontend/badges/dummy_badge.js
index a0dee89736e..a0dee89736e 100644
--- a/spec/javascripts/badges/dummy_badge.js
+++ b/spec/frontend/badges/dummy_badge.js
diff --git a/spec/javascripts/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index d92155d59b5..921c21cb55e 100644
--- a/spec/javascripts/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
-import testAction from 'spec/helpers/vuex_action_helper';
+import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import actions, { transformBackendBadge } from '~/badges/store/actions';
import mutationTypes from '~/badges/store/mutation_types';
@@ -76,7 +76,7 @@ describe('Badges store actions', () => {
beforeEach(() => {
endpointMock = axiosMock.onPost(dummyEndpointUrl);
- dispatch = jasmine.createSpy('dispatch');
+ dispatch = jest.fn();
badgeInAddForm = createDummyBadge();
state = {
...state,
@@ -96,8 +96,8 @@ describe('Badges store actions', () => {
}),
);
- expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
+ dispatch.mockClear();
return [200, dummyResponse];
});
@@ -105,7 +105,7 @@ describe('Badges store actions', () => {
actions
.addBadge({ state, dispatch })
.then(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadge', dummyBadge]]);
+ expect(dispatch.mock.calls).toEqual([['receiveNewBadge', dummyBadge]]);
})
.then(done)
.catch(done.fail);
@@ -121,8 +121,8 @@ describe('Badges store actions', () => {
}),
);
- expect(dispatch.calls.allArgs()).toEqual([['requestNewBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
+ dispatch.mockClear();
return [500, ''];
});
@@ -130,7 +130,7 @@ describe('Badges store actions', () => {
.addBadge({ state, dispatch })
.then(() => done.fail('Expected Ajax call to fail!'))
.catch(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveNewBadgeError']]);
+ expect(dispatch.mock.calls).toEqual([['receiveNewBadgeError']]);
})
.then(done)
.catch(done.fail);
@@ -182,20 +182,20 @@ describe('Badges store actions', () => {
beforeEach(() => {
endpointMock = axiosMock.onDelete(`${dummyEndpointUrl}/${badgeId}`);
- dispatch = jasmine.createSpy('dispatch');
+ dispatch = jest.fn();
});
it('dispatches requestDeleteBadge and receiveDeleteBadge for successful response', done => {
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
+ dispatch.mockClear();
return [200, ''];
});
actions
.deleteBadge({ state, dispatch }, { id: badgeId })
.then(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadge', badgeId]]);
+ expect(dispatch.mock.calls).toEqual([['receiveDeleteBadge', badgeId]]);
})
.then(done)
.catch(done.fail);
@@ -203,8 +203,8 @@ describe('Badges store actions', () => {
it('dispatches requestDeleteBadge and receiveDeleteBadgeError for error response', done => {
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestDeleteBadge', badgeId]]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
+ dispatch.mockClear();
return [500, ''];
});
@@ -212,7 +212,7 @@ describe('Badges store actions', () => {
.deleteBadge({ state, dispatch }, { id: badgeId })
.then(() => done.fail('Expected Ajax call to fail!'))
.catch(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveDeleteBadgeError', badgeId]]);
+ expect(dispatch.mock.calls).toEqual([['receiveDeleteBadgeError', badgeId]]);
})
.then(done)
.catch(done.fail);
@@ -280,7 +280,7 @@ describe('Badges store actions', () => {
beforeEach(() => {
endpointMock = axiosMock.onGet(dummyEndpointUrl);
- dispatch = jasmine.createSpy('dispatch');
+ dispatch = jest.fn();
});
it('dispatches requestLoadBadges and receiveLoadBadges for successful response', done => {
@@ -291,8 +291,8 @@ describe('Badges store actions', () => {
createDummyBadgeResponse(),
];
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
+ dispatch.mockClear();
return [200, dummyReponse];
});
@@ -301,7 +301,7 @@ describe('Badges store actions', () => {
.then(() => {
const badges = dummyReponse.map(transformBackendBadge);
- expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadges', badges]]);
+ expect(dispatch.mock.calls).toEqual([['receiveLoadBadges', badges]]);
})
.then(done)
.catch(done.fail);
@@ -310,8 +310,8 @@ describe('Badges store actions', () => {
it('dispatches requestLoadBadges and receiveLoadBadgesError for error response', done => {
const dummyData = 'this is just some data';
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestLoadBadges', dummyData]]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
+ dispatch.mockClear();
return [500, ''];
});
@@ -319,7 +319,7 @@ describe('Badges store actions', () => {
.loadBadges({ state, dispatch }, dummyData)
.then(() => done.fail('Expected Ajax call to fail!'))
.catch(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveLoadBadgesError']]);
+ expect(dispatch.mock.calls).toEqual([['receiveLoadBadgesError']]);
})
.then(done)
.catch(done.fail);
@@ -382,11 +382,11 @@ describe('Badges store actions', () => {
`image_url=${encodeURIComponent(badgeInForm.imageUrl)}`,
].join('&');
endpointMock = axiosMock.onGet(`${dummyEndpointUrl}/render?${urlParameters}`);
- dispatch = jasmine.createSpy('dispatch');
+ dispatch = jest.fn();
});
it('returns immediately if imageUrl is empty', done => {
- spyOn(axios, 'get');
+ jest.spyOn(axios, 'get').mockImplementation(() => {});
badgeInForm.imageUrl = '';
actions
@@ -399,7 +399,7 @@ describe('Badges store actions', () => {
});
it('returns immediately if linkUrl is empty', done => {
- spyOn(axios, 'get');
+ jest.spyOn(axios, 'get').mockImplementation(() => {});
badgeInForm.linkUrl = '';
actions
@@ -412,19 +412,23 @@ describe('Badges store actions', () => {
});
it('escapes user input', done => {
- spyOn(axios, 'get').and.callFake(() => Promise.resolve({ data: createDummyBadgeResponse() }));
+ jest
+ .spyOn(axios, 'get')
+ .mockImplementation(() => Promise.resolve({ data: createDummyBadgeResponse() }));
badgeInForm.imageUrl = '&make-sandwich=true';
badgeInForm.linkUrl = '<script>I am dangerous!</script>';
actions
.renderBadge({ state, dispatch })
.then(() => {
- expect(axios.get.calls.count()).toBe(1);
- const url = axios.get.calls.argsFor(0)[0];
-
- expect(url).toMatch(`^${dummyEndpointUrl}/render?`);
- expect(url).toMatch('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&');
- expect(url).toMatch('&image_url=%26make-sandwich%3Dtrue$');
+ expect(axios.get.mock.calls.length).toBe(1);
+ const url = axios.get.mock.calls[0][0];
+
+ expect(url).toMatch(new RegExp(`^${dummyEndpointUrl}/render?`));
+ expect(url).toMatch(
+ new RegExp('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&'),
+ );
+ expect(url).toMatch(new RegExp('&image_url=%26make-sandwich%3Dtrue$'));
})
.then(done)
.catch(done.fail);
@@ -433,8 +437,8 @@ describe('Badges store actions', () => {
it('dispatches requestRenderedBadge and receiveRenderedBadge for successful response', done => {
const dummyReponse = createDummyBadgeResponse();
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
+ dispatch.mockClear();
return [200, dummyReponse];
});
@@ -443,7 +447,7 @@ describe('Badges store actions', () => {
.then(() => {
const renderedBadge = transformBackendBadge(dummyReponse);
- expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadge', renderedBadge]]);
+ expect(dispatch.mock.calls).toEqual([['receiveRenderedBadge', renderedBadge]]);
})
.then(done)
.catch(done.fail);
@@ -451,8 +455,8 @@ describe('Badges store actions', () => {
it('dispatches requestRenderedBadge and receiveRenderedBadgeError for error response', done => {
endpointMock.replyOnce(() => {
- expect(dispatch.calls.allArgs()).toEqual([['requestRenderedBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
+ dispatch.mockClear();
return [500, ''];
});
@@ -460,7 +464,7 @@ describe('Badges store actions', () => {
.renderBadge({ state, dispatch })
.then(() => done.fail('Expected Ajax call to fail!'))
.catch(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveRenderedBadgeError']]);
+ expect(dispatch.mock.calls).toEqual([['receiveRenderedBadgeError']]);
})
.then(done)
.catch(done.fail);
@@ -519,7 +523,7 @@ describe('Badges store actions', () => {
badgeInEditForm,
};
endpointMock = axiosMock.onPut(`${dummyEndpointUrl}/${badgeInEditForm.id}`);
- dispatch = jasmine.createSpy('dispatch');
+ dispatch = jest.fn();
});
it('dispatches requestUpdatedBadge and receiveUpdatedBadge for successful response', done => {
@@ -534,8 +538,8 @@ describe('Badges store actions', () => {
}),
);
- expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
+ dispatch.mockClear();
return [200, dummyResponse];
});
@@ -543,7 +547,7 @@ describe('Badges store actions', () => {
actions
.saveBadge({ state, dispatch })
.then(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadge', updatedBadge]]);
+ expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadge', updatedBadge]]);
})
.then(done)
.catch(done.fail);
@@ -559,8 +563,8 @@ describe('Badges store actions', () => {
}),
);
- expect(dispatch.calls.allArgs()).toEqual([['requestUpdatedBadge']]);
- dispatch.calls.reset();
+ expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
+ dispatch.mockClear();
return [500, ''];
});
@@ -568,7 +572,7 @@ describe('Badges store actions', () => {
.saveBadge({ state, dispatch })
.then(() => done.fail('Expected Ajax call to fail!'))
.catch(() => {
- expect(dispatch.calls.allArgs()).toEqual([['receiveUpdatedBadgeError']]);
+ expect(dispatch.mock.calls).toEqual([['receiveUpdatedBadgeError']]);
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/badges/store/mutations_spec.js b/spec/frontend/badges/store/mutations_spec.js
index 8d26f83339d..8d26f83339d 100644
--- a/spec/javascripts/badges/store/mutations_spec.js
+++ b/spec/frontend/badges/store/mutations_spec.js
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
new file mode 100644
index 00000000000..c9375df07e8
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import notesModule from '~/notes/stores/modules';
+import DiscussionCounter from '~/notes/components/discussion_counter.vue';
+import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
+import * as types from '~/notes/stores/mutation_types';
+
+describe('DiscussionCounter component', () => {
+ let store;
+ let wrapper;
+ const localVue = createLocalVue();
+
+ localVue.use(Vuex);
+
+ beforeEach(() => {
+ window.mrTabs = {};
+ const { state, getters, mutations, actions } = notesModule();
+
+ store = new Vuex.Store({
+ state: {
+ ...state,
+ userData: userDataMock,
+ },
+ getters,
+ mutations,
+ actions,
+ });
+ store.dispatch('setNoteableData', {
+ ...noteableDataMock,
+ create_issue_to_resolve_discussions_path: '/test',
+ });
+ store.dispatch('setNotesData', notesDataMock);
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ wrapper = null;
+ });
+
+ describe('has no discussions', () => {
+ it('does not render', () => {
+ wrapper = shallowMount(DiscussionCounter, { store, localVue });
+
+ expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
+ });
+ });
+
+ describe('has no resolvable discussions', () => {
+ it('does not render', () => {
+ store.commit(types.SET_INITIAL_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]);
+ store.dispatch('updateResolvableDiscussionsCounts');
+ wrapper = shallowMount(DiscussionCounter, { store, localVue });
+
+ expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(false);
+ });
+ });
+
+ describe('has resolvable discussions', () => {
+ const updateStore = (note = {}) => {
+ discussionMock.notes[0] = { ...discussionMock.notes[0], ...note };
+ store.commit(types.SET_INITIAL_DISCUSSIONS, [discussionMock]);
+ store.dispatch('updateResolvableDiscussionsCounts');
+ };
+
+ afterEach(() => {
+ delete discussionMock.notes[0].resolvable;
+ delete discussionMock.notes[0].resolved;
+ });
+
+ it('renders', () => {
+ updateStore();
+ wrapper = shallowMount(DiscussionCounter, { store, localVue });
+
+ expect(wrapper.find({ ref: 'discussionCounter' }).exists()).toBe(true);
+ });
+
+ it.each`
+ title | resolved | hasNextBtn | isActive | icon | groupLength
+ ${'hasNextButton'} | ${false} | ${true} | ${false} | ${'check-circle'} | ${2}
+ ${'allResolved'} | ${true} | ${false} | ${true} | ${'check-circle-filled'} | ${0}
+ `('renders correctly if $title', ({ resolved, hasNextBtn, isActive, icon, groupLength }) => {
+ updateStore({ resolvable: true, resolved });
+ wrapper = shallowMount(DiscussionCounter, { store, localVue });
+
+ expect(wrapper.find(`.has-next-btn`).exists()).toBe(hasNextBtn);
+ expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
+ expect(wrapper.find({ name: icon }).exists()).toBe(true);
+ expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
index a00dd445c4f..2ff9dbc5c19 100644
--- a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
+++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
@@ -3,9 +3,12 @@ import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_ne
describe('JumpToNextDiscussionButton', () => {
let wrapper;
+ const fromDiscussionId = 'abc123';
beforeEach(() => {
- wrapper = shallowMount(JumpToNextDiscussionButton);
+ wrapper = shallowMount(JumpToNextDiscussionButton, {
+ propsData: { fromDiscussionId },
+ });
});
afterEach(() => {
@@ -15,4 +18,11 @@ describe('JumpToNextDiscussionButton', () => {
it('matches the snapshot', () => {
expect(wrapper.vm.$el).toMatchSnapshot();
});
+
+ it('calls jumpToNextRelativeDiscussion when clicked', () => {
+ const jumpToNextRelativeDiscussion = jest.fn();
+ wrapper.setMethods({ jumpToNextRelativeDiscussion });
+ wrapper.find({ ref: 'button' }).trigger('click');
+ expect(jumpToNextRelativeDiscussion).toHaveBeenCalledWith(fromDiscussionId);
+ });
});
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
index 74e827784ec..e932133b869 100644
--- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
@@ -1,84 +1,53 @@
/* global Mousetrap */
import 'mousetrap';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
import DiscussionKeyboardNavigator from '~/notes/components/discussion_keyboard_navigator.vue';
-import notesModule from '~/notes/stores/modules';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-const NEXT_ID = 'abc123';
-const PREV_ID = 'def456';
-const NEXT_DIFF_ID = 'abc123_diff';
-const PREV_DIFF_ID = 'def456_diff';
describe('notes/components/discussion_keyboard_navigator', () => {
- let storeOptions;
- let wrapper;
- let store;
+ const localVue = createLocalVue();
- const createComponent = (options = {}) => {
- store = new Vuex.Store(storeOptions);
+ let wrapper;
+ let jumpToNextDiscussion;
+ let jumpToPreviousDiscussion;
+ const createComponent = () => {
wrapper = shallowMount(DiscussionKeyboardNavigator, {
- localVue,
- store,
- ...options,
+ mixins: [
+ localVue.extend({
+ methods: {
+ jumpToNextDiscussion,
+ jumpToPreviousDiscussion,
+ },
+ }),
+ ],
});
-
- wrapper.vm.jumpToDiscussion = jest.fn();
};
beforeEach(() => {
- const notes = notesModule();
-
- notes.getters.nextUnresolvedDiscussionId = () => (currId, isDiff) =>
- isDiff ? NEXT_DIFF_ID : NEXT_ID;
- notes.getters.previousUnresolvedDiscussionId = () => (currId, isDiff) =>
- isDiff ? PREV_DIFF_ID : PREV_ID;
- notes.getters.getDiscussion = () => id => ({ id });
-
- storeOptions = {
- modules: {
- notes,
- },
- };
+ jumpToNextDiscussion = jest.fn();
+ jumpToPreviousDiscussion = jest.fn();
});
afterEach(() => {
wrapper.destroy();
- storeOptions = null;
- store = null;
+ wrapper = null;
});
- describe.each`
- currentAction | expectedNextId | expectedPrevId
- ${'diffs'} | ${NEXT_DIFF_ID} | ${PREV_DIFF_ID}
- ${'show'} | ${NEXT_ID} | ${PREV_ID}
- `('when isDiffView is $isDiffView', ({ currentAction, expectedNextId, expectedPrevId }) => {
+ describe('on mount', () => {
beforeEach(() => {
- window.mrTabs = { currentAction };
createComponent();
});
- afterEach(() => delete window.mrTabs);
it('calls jumpToNextDiscussion when pressing `n`', () => {
Mousetrap.trigger('n');
- expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(
- expect.objectContaining({ id: expectedNextId }),
- );
- expect(wrapper.vm.currentDiscussionId).toEqual(expectedNextId);
+ expect(jumpToNextDiscussion).toHaveBeenCalled();
});
it('calls jumpToPreviousDiscussion when pressing `p`', () => {
Mousetrap.trigger('p');
- expect(wrapper.vm.jumpToDiscussion).toHaveBeenCalledWith(
- expect.objectContaining({ id: expectedPrevId }),
- );
- expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId);
+ expect(jumpToPreviousDiscussion).toHaveBeenCalled();
});
});
@@ -99,13 +68,13 @@ describe('notes/components/discussion_keyboard_navigator', () => {
it('does not call jumpToNextDiscussion when pressing `n`', () => {
Mousetrap.trigger('n');
- expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled();
+ expect(jumpToNextDiscussion).not.toHaveBeenCalled();
});
it('does not call jumpToNextDiscussion when pressing `p`', () => {
Mousetrap.trigger('p');
- expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled();
+ expect(jumpToPreviousDiscussion).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
new file mode 100644
index 00000000000..4e5325b8bc3
--- /dev/null
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -0,0 +1,178 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import * as utils from '~/lib/utils/common_utils';
+import discussionNavigation from '~/notes/mixins/discussion_navigation';
+import eventHub from '~/notes/event_hub';
+import notesModule from '~/notes/stores/modules';
+import { setHTMLFixture } from 'helpers/fixtures';
+
+const discussion = (id, index) => ({
+ id,
+ resolvable: index % 2 === 0,
+ active: true,
+ notes: [{}],
+ diff_discussion: true,
+});
+const createDiscussions = () => [...'abcde'].map(discussion);
+const createComponent = () => ({
+ mixins: [discussionNavigation],
+ render() {
+ return this.$slots.default;
+ },
+});
+
+describe('Discussion navigation mixin', () => {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ let wrapper;
+ let store;
+ let expandDiscussion;
+
+ beforeEach(() => {
+ setHTMLFixture(
+ [...'abcde']
+ .map(
+ id =>
+ `<ul class="notes" data-discussion-id="${id}"></ul>
+ <div class="discussion" data-discussion-id="${id}"></div>`,
+ )
+ .join(''),
+ );
+
+ jest.spyOn(utils, 'scrollToElement');
+
+ expandDiscussion = jest.fn();
+ const { actions, ...notesRest } = notesModule();
+ store = new Vuex.Store({
+ modules: {
+ notes: {
+ ...notesRest,
+ actions: { ...actions, expandDiscussion },
+ },
+ },
+ });
+ store.state.notes.discussions = createDiscussions();
+
+ wrapper = shallowMount(createComponent(), { store, localVue });
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ jest.clearAllMocks();
+ });
+
+ const findDiscussion = (selector, id) =>
+ document.querySelector(`${selector}[data-discussion-id="${id}"]`);
+
+ describe('cycle through discussions', () => {
+ beforeEach(() => {
+ // eslint-disable-next-line new-cap
+ window.mrTabs = { eventHub: new localVue(), tabShown: jest.fn() };
+ });
+
+ describe.each`
+ fn | args | currentId | expected
+ ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'}
+ ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'}
+ ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'}
+ ${'jumpToNextRelativeDiscussion'} | ${[null]} | ${null} | ${'a'}
+ ${'jumpToNextRelativeDiscussion'} | ${['a']} | ${null} | ${'c'}
+ ${'jumpToNextRelativeDiscussion'} | ${['e']} | ${'c'} | ${'a'}
+ `('$fn (args = $args, currentId = $currentId)', ({ fn, args, currentId, expected }) => {
+ beforeEach(() => {
+ store.state.notes.currentDiscussionId = currentId;
+ });
+
+ describe('on `show` active tab', () => {
+ beforeEach(() => {
+ window.mrTabs.currentAction = 'show';
+ wrapper.vm[fn](...args);
+ });
+
+ it('sets current discussion', () => {
+ expect(store.state.notes.currentDiscussionId).toEqual(expected);
+ });
+
+ it('expands discussion', () => {
+ expect(expandDiscussion).toHaveBeenCalled();
+ });
+
+ it('scrolls to element', () => {
+ expect(utils.scrollToElement).toHaveBeenCalledWith(
+ findDiscussion('div.discussion', expected),
+ );
+ });
+ });
+
+ describe('on `diffs` active tab', () => {
+ beforeEach(() => {
+ window.mrTabs.currentAction = 'diffs';
+ wrapper.vm[fn](...args);
+ });
+
+ it('sets current discussion', () => {
+ expect(store.state.notes.currentDiscussionId).toEqual(expected);
+ });
+
+ it('expands discussion', () => {
+ expect(expandDiscussion).toHaveBeenCalled();
+ });
+
+ it('scrolls when scrollToDiscussion is emitted', () => {
+ expect(utils.scrollToElement).not.toHaveBeenCalled();
+
+ eventHub.$emit('scrollToDiscussion');
+
+ expect(utils.scrollToElement).toHaveBeenCalledWith(findDiscussion('ul.notes', expected));
+ });
+ });
+
+ describe('on `other` active tab', () => {
+ beforeEach(() => {
+ window.mrTabs.currentAction = 'other';
+ wrapper.vm[fn](...args);
+ });
+
+ it('sets current discussion', () => {
+ expect(store.state.notes.currentDiscussionId).toEqual(expected);
+ });
+
+ it('does not expand discussion yet', () => {
+ expect(expandDiscussion).not.toHaveBeenCalled();
+ });
+
+ it('shows mrTabs', () => {
+ expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show');
+ });
+
+ describe('when tab is changed', () => {
+ beforeEach(() => {
+ window.mrTabs.eventHub.$emit('MergeRequestTabChange');
+
+ jest.runAllTimers();
+ });
+
+ it('expands discussion', () => {
+ expect(expandDiscussion).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ discussionId: expected,
+ },
+ undefined,
+ );
+ });
+
+ it('scrolls to discussion', () => {
+ expect(utils.scrollToElement).toHaveBeenCalledWith(
+ findDiscussion('div.discussion', expected),
+ );
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js
deleted file mode 100644
index 9c7aed43a3b..00000000000
--- a/spec/javascripts/notes/components/discussion_counter_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import createStore from '~/notes/stores';
-import DiscussionCounter from '~/notes/components/discussion_counter.vue';
-import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
-
-describe('DiscussionCounter component', () => {
- let store;
- let vm;
- const notes = { currentDiscussionId: null };
-
- beforeEach(() => {
- window.mrTabs = {};
-
- const Component = Vue.extend(DiscussionCounter);
-
- store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
-
- vm = createComponentWithStore(Component, store);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('methods', () => {
- describe('jumpToNextDiscussion', () => {
- it('expands unresolved discussion', () => {
- window.mrTabs.currentAction = 'show';
-
- spyOn(vm, 'expandDiscussion').and.stub();
- const discussions = [
- {
- ...discussionMock,
- id: discussionMock.id,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
- resolved: true,
- },
- {
- ...discussionMock,
- id: discussionMock.id + 1,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }],
- resolved: false,
- },
- ];
- const firstDiscussionId = discussionMock.id + 1;
- store.replaceState({
- ...store.state,
- discussions,
- notes,
- });
- vm.jumpToNextDiscussion();
-
- expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: firstDiscussionId });
- });
-
- it('jumps to next unresolved discussion from diff tab if all diff discussions are resolved', () => {
- window.mrTabs.currentAction = 'diff';
- spyOn(vm, 'switchToDiscussionsTabAndJumpTo').and.stub();
-
- const unresolvedId = discussionMock.id + 1;
- const discussions = [
- {
- ...discussionMock,
- id: discussionMock.id,
- diff_discussion: true,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
- resolved: true,
- },
- {
- ...discussionMock,
- id: unresolvedId,
- notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }],
- resolved: false,
- },
- ];
- store.replaceState({
- ...store.state,
- discussions,
- notes,
- });
- vm.jumpToNextDiscussion();
-
- expect(vm.switchToDiscussionsTabAndJumpTo).toHaveBeenCalledWith(unresolvedId);
- });
- });
- });
-});
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 1d652f703b8..7d98f8a0c3e 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -271,6 +271,7 @@ MergeRequest::Metrics:
- diff_size
- modified_paths_size
- commits_count
+- first_approved_at
Ci::Pipeline:
- id
- project_id
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index efa7fd4b975..da6a6a9149b 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -37,5 +37,40 @@ describe Gitlab::JobWaiter do
expect(result).to contain_exactly('a')
end
+
+ context 'when a label is provided' do
+ let(:waiter) { described_class.new(2, worker_label: 'Foo') }
+ let(:started_total) { double(:started_total) }
+ let(:timeouts_total) { double(:timeouts_total) }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(described_class::STARTED_METRIC, anything)
+ .and_return(started_total)
+
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(described_class::TIMEOUTS_METRIC, anything)
+ .and_return(timeouts_total)
+ end
+
+ it 'increments just job_waiter_started_total when all jobs complete' do
+ expect(started_total).to receive(:increment).with(worker: 'Foo')
+ expect(timeouts_total).not_to receive(:increment)
+
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
+
+ result = nil
+ expect { Timeout.timeout(1) { result = waiter.wait(2) } }.not_to raise_error
+ end
+
+ it 'increments job_waiter_started_total and job_waiter_timeouts_total when it times out' do
+ expect(started_total).to receive(:increment).with(worker: 'Foo')
+ expect(timeouts_total).to receive(:increment).with(worker: 'Foo')
+
+ result = nil
+ expect { Timeout.timeout(2) { result = waiter.wait(1) } }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb
deleted file mode 100644
index e913bb600ec..00000000000
--- a/spec/lib/gitlab/tracing_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-describe Gitlab::Tracing do
- using RSpec::Parameterized::TableSyntax
-
- describe '.enabled?' do
- where(:connection_string, :enabled_state) do
- nil | false
- "" | false
- "opentracing://jaeger" | true
- end
-
- with_them do
- it 'returns the correct state for .enabled?' do
- expect(described_class).to receive(:connection_string).and_return(connection_string)
-
- expect(described_class.enabled?).to eq(enabled_state)
- end
- end
- end
-
- describe '.tracing_url_enabled?' do
- where(:enabled?, :tracing_url_template, :tracing_url_enabled_state) do
- false | nil | false
- false | "" | false
- false | "http://localhost" | false
- true | nil | false
- true | "" | false
- true | "http://localhost" | true
- end
-
- with_them do
- it 'returns the correct state for .tracing_url_enabled?' do
- expect(described_class).to receive(:enabled?).and_return(enabled?)
- allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
-
- expect(described_class.tracing_url_enabled?).to eq(tracing_url_enabled_state)
- end
- end
- end
-
- describe '.tracing_url' do
- where(:tracing_url_enabled?, :tracing_url_template, :correlation_id, :process_name, :tracing_url) do
- false | "https://localhost" | "123" | "web" | nil
- true | "https://localhost" | "123" | "web" | "https://localhost"
- true | "https://localhost?service={{ service }}" | "123" | "web" | "https://localhost?service=web"
- true | "https://localhost?c={{ correlation_id }}" | "123" | "web" | "https://localhost?c=123"
- true | "https://localhost?c={{ correlation_id }}&s={{ service }}" | "123" | "web" | "https://localhost?c=123&s=web"
- true | "https://localhost?c={{ correlation_id }}" | nil | "web" | "https://localhost?c="
- true | "https://localhost?c={{ correlation_id }}&s=%22{{ service }}%22" | "123" | "web" | "https://localhost?c=123&s=%22web%22"
- true | "https://localhost?c={{correlation_id}}&s={{service}}" | "123" | "web" | "https://localhost?c=123&s=web"
- true | "https://localhost?c={{correlation_id }}&s={{ service}}" | "123" | "web" | "https://localhost?c=123&s=web"
- end
-
- with_them do
- it 'returns the correct state for .tracing_url' do
- expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?)
- allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
- allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return(correlation_id)
- allow(Gitlab).to receive(:process_name).and_return(process_name)
-
- expect(described_class.tracing_url).to eq(tracing_url)
- end
- end
- end
-end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4bfb5771bb8..37219365ecf 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2509,6 +2509,64 @@ describe Ci::Build do
end
end
+ describe 'CHANGED_PAGES variables' do
+ let(:route_map_yaml) do
+ <<~ROUTEMAP
+ - source: 'bar/branch-test.txt'
+ public: '/bar/branches'
+ ROUTEMAP
+ end
+
+ before do
+ allow_any_instance_of(Project)
+ .to receive(:route_map_for).with(/.+/)
+ .and_return(Gitlab::RouteMap.new(route_map_yaml))
+ end
+
+ context 'with a deployment environment and a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:environment) { create(:environment, project: merge_request.project, name: "foo-#{project.default_branch}") }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
+
+ it 'populates CI_MERGE_REQUEST_CHANGED_PAGES_* variables' do
+ expect(subject).to include(
+ { key: 'CI_MERGE_REQUEST_CHANGED_PAGE_PATHS', value: '/bar/branches', public: true, masked: false },
+ { key: 'CI_MERGE_REQUEST_CHANGED_PAGE_URLS', value: File.join(environment.external_url, '/bar/branches'), public: true, masked: false }
+ )
+ end
+
+ context 'with a deployment environment and no merge request' do
+ let(:environment) { create(:environment, project: project, name: "foo-#{project.default_branch}") }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: environment.name) }
+
+ it 'does not append CHANGED_PAGES variables' do
+ ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ }
+
+ expect(ci_variables).to be_empty
+ end
+ end
+
+ context 'with no deployment environment and a present merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_project: project, target_project: project) }
+ let(:build) { create(:ci_build, pipeline: merge_request.all_pipelines.take) }
+
+ it 'does not append CHANGED_PAGES variables' do
+ ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ }
+
+ expect(ci_variables).to be_empty
+ end
+ end
+
+ context 'with no deployment environment and no merge request' do
+ it 'does not append CHANGED_PAGES variables' do
+ ci_variables = subject.select { |var| var[:key] =~ /MERGE_REQUEST_CHANGED_PAGES/ }
+
+ expect(ci_variables).to be_empty
+ end
+ end
+ end
+ end
+
context 'when build has user' do
let(:user_variables) do
[
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 0f2c6928820..10283b54796 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -51,8 +51,10 @@ describe EnvironmentStatus do
# - source: /files\/(.+)/
# public: '\1'
describe '#changes' do
+ subject { environment_status.changes }
+
it 'contains only added and modified public pages' do
- expect(environment_status.changes).to contain_exactly(
+ expect(subject).to contain_exactly(
{
path: 'ruby-style-guide.html',
external_url: "#{environment.external_url}/ruby-style-guide.html"
@@ -64,6 +66,18 @@ describe EnvironmentStatus do
end
end
+ describe '#changed_paths' do
+ subject { environment_status.changed_urls }
+
+ it { is_expected.to contain_exactly("#{environment.external_url}/ruby-style-guide.html", "#{environment.external_url}/html/page.html") }
+ end
+
+ describe '#changed_urls' do
+ subject { environment_status.changed_paths }
+
+ it { is_expected.to contain_exactly('ruby-style-guide.html', 'html/page.html') }
+ end
+
describe '.for_merge_request' do
let(:admin) { create(:admin) }
let!(:pipeline) { create(:ci_pipeline, sha: sha, merge_requests_as_head_pipeline: [merge_request]) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 7b24ca5eb23..33459767302 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -643,4 +643,12 @@ describe PagesDomain do
end
end
end
+
+ describe '.find_by_domain_case_insensitive' do
+ it 'lookup is case-insensitive' do
+ pages_domain = create(:pages_domain, domain: "Pages.IO")
+
+ expect(PagesDomain.find_by_domain_case_insensitive('pages.io')).to eq(pages_domain)
+ end
+ end
end
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 45ea4cd74ed..64c7a9b230d 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -74,5 +74,28 @@ describe ChatNotificationService do
chat_service.execute(data)
end
end
+
+ shared_examples 'with channel specified' do |channel, expected_channels|
+ before do
+ allow(chat_service).to receive(:push_channel).and_return(channel)
+ end
+
+ it 'notifies all channels' do
+ expect(chat_service).to receive(:notify).with(any_args, hash_including(channel: expected_channels)).and_return(true)
+ expect(chat_service.execute(data)).to be(true)
+ end
+ end
+
+ context 'with single channel specified' do
+ it_behaves_like 'with channel specified', 'slack-integration', ['slack-integration']
+ end
+
+ context 'with multiple channel names specified' do
+ it_behaves_like 'with channel specified', 'slack-integration,#slack-test', ['slack-integration', '#slack-test']
+ end
+
+ context 'with multiple channel names with spaces specified' do
+ it_behaves_like 'with channel specified', 'slack-integration, #slack-test, @UDLP91W0A', ['slack-integration', '#slack-test', '@UDLP91W0A']
+ end
end
end
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 99cb2bfe221..44f7115f6a8 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -141,21 +141,29 @@ describe API::Internal::Pages do
context 'custom domain' do
let(:namespace) { create(:namespace, name: 'gitlab-org') }
let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
- let!(:pages_domain) { create(:pages_domain, domain: 'pages.gitlab.io', project: project) }
+ let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) }
context 'when there are no pages deployed for the related project' do
it 'responds with 204 No Content' do
- query_host('pages.gitlab.io')
+ query_host('pages.io')
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when there are pages deployed for the related project' do
+ it 'domain lookup is case insensitive' do
+ deploy_pages(project)
+
+ query_host('Pages.IO')
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
it 'responds with the correct domain configuration' do
deploy_pages(project)
- query_host('pages.gitlab.io')
+ query_host('pages.io')
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
index 2b68e7bfa82..24ff57c8517 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
@@ -151,22 +151,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it 'uses the username as an option for slack when configured' do
allow(chat_service).to receive(:username).and_return(username)
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, username: username, http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(username: username)
chat_service.execute(data)
end
it 'uses the channel as an option when it is configured' do
allow(chat_service).to receive(:channel).and_return(channel)
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: channel, http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: [channel])
chat_service.execute(data)
end
@@ -174,11 +166,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it "uses the right channel for push event" do
chat_service.update(push_channel: "random")
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random", http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
chat_service.execute(data)
end
@@ -186,11 +174,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it "uses the right channel for merge request event" do
chat_service.update(merge_request_channel: "random")
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random", http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
chat_service.execute(@merge_sample_data)
end
@@ -198,11 +182,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it "uses the right channel for issue event" do
chat_service.update(issue_channel: "random")
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random", http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
chat_service.execute(@issues_sample_data)
end
@@ -213,7 +193,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it "uses confidential issue channel" do
chat_service.update(confidential_issue_channel: 'confidential')
- expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+ expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
chat_service.execute(@issues_sample_data)
end
@@ -221,7 +201,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it 'falls back to issue channel' do
chat_service.update(issue_channel: 'fallback_channel')
- expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+ expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
chat_service.execute(@issues_sample_data)
end
@@ -230,11 +210,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
it "uses the right channel for wiki event" do
chat_service.update(wiki_page_channel: "random")
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random", http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
chat_service.execute(@wiki_page_sample_data)
end
@@ -249,11 +225,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random", http_client: SlackService::Notifier::HTTPClient)
- .and_return(
- double(:slack_service).as_null_object
- )
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
chat_service.execute(note_data)
end
@@ -268,7 +240,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
- expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+ expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
chat_service.execute(note_data)
end
@@ -278,7 +250,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
- expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+ expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
chat_service.execute(note_data)
end