summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-09-23 21:06:29 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-09-23 21:06:29 +0000
commitb9254657872c4db441ab268154686f5476fb4bc6 (patch)
treeab045c623296a049d1246ba2d66800456a1077aa
parentc792263edfaf826c58f4aa41d26904464a17a3e7 (diff)
downloadgitlab-ce-b9254657872c4db441ab268154686f5476fb4bc6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js7
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue8
-rw-r--r--app/helpers/boards_helper.rb2
-rw-r--r--app/services/boards/lists/update_service.rb10
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--changelogs/unreleased/32427-cannot-reorder-issue-boards.yml5
-rw-r--r--changelogs/unreleased/32595-blame-or-history-newline-in-filename.yml5
-rw-r--r--config/routes/repository.rb2
-rw-r--r--locale/gitlab.pot42
-rw-r--r--qa/qa/page/merge_request/show.rb21
-rw-r--r--qa/qa/page/project/settings/main.rb7
-rw-r--r--qa/qa/page/project/settings/merge_request.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb8
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb4
-rw-r--r--spec/frontend/ide/components/error_message_spec.js122
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/recaptcha_modal_spec.js36
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js106
-rw-r--r--spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js14
-rw-r--r--spec/routing/project_routing_spec.rb5
-rw-r--r--spec/services/boards/lists/update_service_spec.rb66
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb4
29 files changed, 371 insertions, 206 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 992c5e5e330..d57be10f472 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -74,6 +74,11 @@ const Api = {
});
},
+ groupLabels(namespace) {
+ const url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
+ return axios.get(url).then(({ data }) => data);
+ },
+
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 7744984edfc..ac903d60089 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -48,6 +48,8 @@ export default () => {
import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
AddStageButton: () =>
import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'),
+ CustomStageFormContainer: () =>
+ import('ee_component/analytics/cycle_analytics/components/custom_stage_form_container.vue'),
},
mixins: [filterMixins, addStageMixin],
data() {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index a4715789337..e0832ab535d 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -537,13 +537,6 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
};
/**
- * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
- * the first non-zero unit/value pair.
- */
-export const abbreviateTime = timeStr =>
- timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
-
-/**
* Calculates the milliseconds between now and a given date string.
* The result cannot become negative.
*
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 24d5b14ded9..65ecd5be05d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,6 +1,5 @@
<script>
import { __, sprintf } from '~/locale';
-import { abbreviateTime } from '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -41,12 +40,6 @@ export default {
},
},
computed: {
- timeSpent() {
- return this.abbreviateTime(this.timeSpentHumanReadable);
- },
- timeEstimate() {
- return this.abbreviateTime(this.timeEstimateHumanReadable);
- },
divClass() {
if (this.showComparisonState) {
return 'compare';
@@ -73,11 +66,11 @@ export default {
},
text() {
if (this.showComparisonState) {
- return `${this.timeSpent} / ${this.timeEstimate}`;
+ return `${this.timeSpentHumanReadable} / ${this.timeEstimateHumanReadable}`;
} else if (this.showEstimateOnlyState) {
- return `-- / ${this.timeEstimate}`;
+ return `-- / ${this.timeEstimateHumanReadable}`;
} else if (this.showSpentOnlyState) {
- return `${this.timeSpent} / --`;
+ return `${this.timeSpentHumanReadable} / --`;
} else if (this.showNoTimeTrackingState) {
return __('None');
}
@@ -100,11 +93,6 @@ export default {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
},
- methods: {
- abbreviateTime(timeStr) {
- return abbreviateTime(timeStr);
- },
- },
};
</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 52acd1de666..ebedd4842c9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -110,7 +110,10 @@ export default {
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
<div class="media-body">
- <div class="font-weight-bold js-pipeline-info-container">
+ <div
+ class="font-weight-bold js-pipeline-info-container"
+ data-qa-selector="merge_request_pipeline_info_content"
+ >
{{ pipeline.details.name }}
<gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</gl-link
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
new file mode 100644
index 00000000000..a4e004c3341
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+// see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml
+export const callbackName = 'recaptchaDialogCallback';
+
+export const eventHub = new Vue();
+
+const throwDuplicateCallbackError = () => {
+ throw new Error(`${callbackName} is already defined!`);
+};
+
+if (window[callbackName]) {
+ throwDuplicateCallbackError();
+}
+
+const callback = () => eventHub.$emit('submit');
+
+Object.defineProperty(window, callbackName, {
+ get: () => callback,
+ set: throwDuplicateCallbackError,
+});
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index f0aae20477b..55172649813 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,5 +1,6 @@
<script>
import DeprecatedModal from './deprecated_modal.vue';
+import { eventHub } from './recaptcha_eventhub';
export default {
name: 'RecaptchaModal',
@@ -30,14 +31,11 @@ export default {
},
mounted() {
- if (window.recaptchaDialogCallback) {
- throw new Error('recaptchaDialogCallback is already defined!');
- }
- window.recaptchaDialogCallback = this.submit.bind(this);
+ eventHub.$on('submit', this.submit);
},
beforeDestroy() {
- window.recaptchaDialogCallback = null;
+ eventHub.$off('submit', this.submit);
},
methods: {
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 3f0679f9bf9..d3950219f3f 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -88,7 +88,7 @@ module BoardsHelper
end
def boards_link_text
- if multiple_boards_available?
+ if current_board_parent.multiple_issue_boards_available?
s_("IssueBoards|Boards")
else
s_("IssueBoards|Board")
diff --git a/app/services/boards/lists/update_service.rb b/app/services/boards/lists/update_service.rb
index 2ddeb6f0bd8..ad96e42f756 100644
--- a/app/services/boards/lists/update_service.rb
+++ b/app/services/boards/lists/update_service.rb
@@ -4,10 +4,10 @@ module Boards
module Lists
class UpdateService < Boards::BaseService
def execute(list)
- return not_authorized if preferences? && !can_read?(list)
- return not_authorized if position? && !can_admin?(list)
+ update_preferences_result = update_preferences(list) if can_read?(list)
+ update_position_result = update_position(list) if can_admin?(list)
- if update_preferences(list) || update_position(list)
+ if update_preferences_result || update_position_result
success(list: list)
else
error(list.errors.messages, 422)
@@ -32,10 +32,6 @@ module Boards
{ collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
end
- def not_authorized
- error("Not authorized", 403)
- end
-
def preferences?
params.has_key?(:collapsed)
end
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index b60b5d55a1b..31fd12d191e 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -9,7 +9,7 @@
= _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
.settings-content
- = form_for @application_setting, url: integrations_admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index de994250649..c9458475aa5 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -6,7 +6,7 @@
.issues-filters{ class: ("w-100" if type == :boards_modal) }
.issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal }
- - if type == :boards && (multiple_boards_available? || current_board_parent.boards.size > 1)
+ - if type == :boards
= render "shared/boards/switcher", board: board
= form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
- if params[:search].present?
diff --git a/changelogs/unreleased/32427-cannot-reorder-issue-boards.yml b/changelogs/unreleased/32427-cannot-reorder-issue-boards.yml
new file mode 100644
index 00000000000..0ad9279876e
--- /dev/null
+++ b/changelogs/unreleased/32427-cannot-reorder-issue-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Fix ordering of issue board lists not being persisted
+merge_request: 17356
+author:
+type: fixed
diff --git a/changelogs/unreleased/32595-blame-or-history-newline-in-filename.yml b/changelogs/unreleased/32595-blame-or-history-newline-in-filename.yml
new file mode 100644
index 00000000000..b67b823704f
--- /dev/null
+++ b/changelogs/unreleased/32595-blame-or-history-newline-in-filename.yml
@@ -0,0 +1,5 @@
+---
+title: Users can view the blame or history of a file with newlines in its filename.
+merge_request: 17543
+author: Jesse Hall @jessehall3
+type: fixed
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 093b86f3259..4815575ba9f 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -34,7 +34,7 @@ scope format: false do
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
id: /.*/,
- path: /.*/
+ path: /[^\0]*/
}
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6658d2b67e5..dd3c2d96d7f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4547,6 +4547,42 @@ msgstr ""
msgid "CustomCycleAnalytics|Add a stage"
msgstr ""
+msgid "CustomCycleAnalytics|Add stage"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Enter a name for the stage"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Name"
+msgstr ""
+
+msgid "CustomCycleAnalytics|New stage"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Please select a start event first"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Select start event"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Select stop event"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Start event"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Start event changed, please select a valid stop event"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Start event label"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Stop event"
+msgstr ""
+
+msgid "CustomCycleAnalytics|Stop event label"
+msgstr ""
+
msgid "Customize colors"
msgstr ""
@@ -10208,9 +10244,6 @@ msgstr ""
msgid "New snippet"
msgstr ""
-msgid "New stage"
-msgstr ""
-
msgid "New subgroup"
msgstr ""
@@ -15698,6 +15731,9 @@ msgstr ""
msgid "There was an error fetching configuration for charts"
msgstr ""
+msgid "There was an error fetching the form data"
+msgstr ""
+
msgid "There was an error gathering the chart data"
msgstr ""
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 6e550805f9f..ea0cbfe2ab0 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -6,6 +6,16 @@ module QA
class Show < Page::Base
include Page::Component::Note
+ view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue' do
+ element :dropdown_toggle
+ element :download_email_patches
+ element :download_plain_diff
+ end
+
+ view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue' do
+ element :merge_request_pipeline_info_content
+ end
+
view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do
element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
@@ -27,12 +37,6 @@ module QA
element :squash_checkbox
end
- view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue' do
- element :dropdown_toggle
- element :download_email_patches
- element :download_plain_diff
- end
-
view 'app/views/projects/merge_requests/show.html.haml' do
element :notes_tab
element :diffs_tab
@@ -111,6 +115,11 @@ module QA
end
end
+ def has_pipeline_status?(text)
+ # Pipelines can be slow, so we wait a bit longer than the usual 10 seconds
+ has_element?(:merge_request_pipeline_info_content, text: text, wait: 30)
+ end
+
def has_title?(title)
has_element?(:title, text: title)
end
diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb
index 6b26c82a46f..3a60330217b 100644
--- a/qa/qa/page/project/settings/main.rb
+++ b/qa/qa/page/project/settings/main.rb
@@ -11,6 +11,7 @@ module QA
view 'app/views/projects/edit.html.haml' do
element :advanced_settings
+ element :merge_request_settings
end
view 'app/views/projects/settings/_general.html.haml' do
@@ -41,6 +42,12 @@ module QA
end
end
+ def expand_merge_requests_settings(&block)
+ expand_section(:merge_request_settings) do
+ MergeRequest.perform(&block)
+ end
+ end
+
def expand_visibility_project_features_permissions(&block)
expand_section(:visibility_features_permissions_content) do
VisibilityFeaturesPermissions.perform(&block)
diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb
index f92528c4262..7da2c9d168c 100644
--- a/qa/qa/page/project/settings/merge_request.rb
+++ b/qa/qa/page/project/settings/merge_request.rb
@@ -8,7 +8,6 @@ module QA
include Common
view 'app/views/projects/edit.html.haml' do
- element :merge_request_settings
element :save_merge_request_changes
end
@@ -16,14 +15,18 @@ module QA
element :radio_button_merge_ff
end
+ def click_save_changes
+ click_element :save_merge_request_changes
+ end
+
def enable_ff_only
- expand_section(:merge_request_settings) do
- click_element :radio_button_merge_ff
- click_element :save_merge_request_changes
- end
+ click_element :radio_button_merge_ff
+ click_save_changes
end
end
end
end
end
end
+
+QA::Page::Project::Settings::MergeRequest.prepend_if_ee("QA::EE::Page::Project::Settings::MergeRequest")
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 3ce291bf8bc..c7b5e40d0be 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -13,8 +13,12 @@ module QA
end
project.visit!
- Page::Project::Menu.perform(&:click_settings)
- Page::Project::Settings::MergeRequest.perform(&:enable_ff_only)
+ Page::Project::Menu.perform(&:go_to_general_settings)
+ Page::Project::Settings::Main.perform do |main|
+ main.expand_merge_requests_settings do |settings|
+ settings.enable_ff_only
+ end
+ end
merge_request = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index 802fc1770a4..77b5872fcb3 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -162,10 +162,10 @@ describe Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
+ it 'returns a 422 unprocessable entity response' do
move user: guest, board: board, list: planning, position: 6
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(422)
end
end
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
new file mode 100644
index 00000000000..fd6d1a494d4
--- /dev/null
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -0,0 +1,122 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import ErrorMessage from '~/ide/components/error_message.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IDE error message component', () => {
+ let wrapper;
+
+ const setErrorMessageMock = jest.fn();
+ const createComponent = messageProps => {
+ const fakeStore = new Vuex.Store({
+ actions: { setErrorMessage: setErrorMessageMock },
+ });
+
+ wrapper = shallowMount(ErrorMessage, {
+ propsData: {
+ message: {
+ text: 'some text',
+ actionText: 'test action',
+ actionPayload: 'testActionPayload',
+ ...messageProps,
+ },
+ },
+ store: fakeStore,
+ localVue,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ setErrorMessageMock.mockReset();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders error message', () => {
+ const text = 'error message';
+ createComponent({ text });
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('clears error message on click', () => {
+ createComponent();
+ wrapper.trigger('click');
+
+ expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null, undefined);
+ });
+
+ describe('with action', () => {
+ let actionMock;
+
+ const message = {
+ actionText: 'test action',
+ actionPayload: 'testActionPayload',
+ };
+
+ beforeEach(() => {
+ actionMock = jest.fn().mockResolvedValue();
+ createComponent({
+ ...message,
+ action: actionMock,
+ });
+ });
+
+ it('renders action button', () => {
+ const button = wrapper.find('button');
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toContain(message.actionText);
+ });
+
+ it('does not clear error message on click', () => {
+ wrapper.trigger('click');
+
+ expect(setErrorMessageMock).not.toHaveBeenCalled();
+ });
+
+ it('dispatches action', () => {
+ wrapper.find('button').trigger('click');
+
+ expect(actionMock).toHaveBeenCalledWith(message.actionPayload);
+ });
+
+ it('does not dispatch action when already loading', () => {
+ wrapper.find('button').trigger('click');
+ actionMock.mockReset();
+ wrapper.find('button').trigger('click');
+ expect(actionMock).not.toHaveBeenCalled();
+ });
+
+ it('shows loading icon when loading', () => {
+ let resolve;
+ actionMock.mockImplementation(
+ () =>
+ new Promise(ok => {
+ resolve = ok;
+ }),
+ );
+ wrapper.find('button').trigger('click');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
+ resolve();
+ });
+ });
+
+ it('hides loading icon when operation finishes', () => {
+ wrapper.find('button').trigger('click');
+ return actionMock()
+ .then(() => wrapper.vm.$nextTick())
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 9f1700bb243..8cbf170b3e6 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -388,20 +388,6 @@ describe('prettyTime methods', () => {
expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
});
});
-
- describe('abbreviateTime', () => {
- it('should abbreviate stringified times for weeks', () => {
- const fullTimeString = '1w 3d 4h 5m';
-
- expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', () => {
- const fullTimeString = '0w 3d 4h 5m';
-
- expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d');
- });
- });
});
describe('calculateRemainingMilliseconds', () => {
diff --git a/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js b/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js
new file mode 100644
index 00000000000..d86d627886f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/recaptcha_eventhub_spec.js
@@ -0,0 +1,21 @@
+import { eventHub, callbackName } from '~/vue_shared/components/recaptcha_eventhub';
+
+describe('reCAPTCHA event hub', () => {
+ // the following test case currently crashes
+ // see https://gitlab.com/gitlab-org/gitlab/issues/29192#note_217840035
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('throws an error for overriding the callback', () => {
+ expect(() => {
+ window[callbackName] = 'something';
+ }).toThrow();
+ });
+
+ it('triggering callback emits a submit event', () => {
+ const eventHandler = jest.fn();
+ eventHub.$once('submit', eventHandler);
+
+ window[callbackName]();
+
+ expect(eventHandler).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/recaptcha_modal_spec.js b/spec/frontend/vue_shared/components/recaptcha_modal_spec.js
new file mode 100644
index 00000000000..e509fe09d94
--- /dev/null
+++ b/spec/frontend/vue_shared/components/recaptcha_modal_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { eventHub } from '~/vue_shared/components/recaptcha_eventhub';
+
+import RecaptchaModal from '~/vue_shared/components/recaptcha_modal';
+
+describe('RecaptchaModal', () => {
+ const recaptchaFormId = 'recaptcha-form';
+ const recaptchaHtml = `<form id="${recaptchaFormId}"></form>`;
+
+ let wrapper;
+
+ const findRecaptchaForm = () => wrapper.find(`#${recaptchaFormId}`).element;
+
+ beforeEach(() => {
+ wrapper = shallowMount(RecaptchaModal, {
+ sync: false,
+ propsData: {
+ html: recaptchaHtml,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('submits the form if event hub emits submit event', () => {
+ const form = findRecaptchaForm();
+ jest.spyOn(form, 'submit').mockImplementation();
+
+ eventHub.$emit('submit');
+
+ expect(form.submit).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
deleted file mode 100644
index 80d6c7fd564..00000000000
--- a/spec/javascripts/ide/components/error_message_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import ErrorMessage from '~/ide/components/error_message.vue';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore } from '../helpers';
-
-describe('IDE error message component', () => {
- const Component = Vue.extend(ErrorMessage);
- let vm;
-
- beforeEach(() => {
- vm = createComponentWithStore(Component, store, {
- message: {
- text: 'error message',
- action: null,
- actionText: null,
- },
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
- });
-
- it('renders error message', () => {
- expect(vm.$el.textContent).toContain('error message');
- });
-
- it('clears error message on click', () => {
- spyOn(vm, 'setErrorMessage');
-
- vm.$el.click();
-
- expect(vm.setErrorMessage).toHaveBeenCalledWith(null);
- });
-
- describe('with action', () => {
- let actionSpy;
-
- beforeEach(done => {
- actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve());
-
- vm.message.action = actionSpy;
- vm.message.actionText = 'test action';
- vm.message.actionPayload = 'testActionPayload';
-
- vm.$nextTick(done);
- });
-
- it('renders action button', () => {
- expect(vm.$el.querySelector('.flash-action')).not.toBe(null);
- expect(vm.$el.textContent).toContain('test action');
- });
-
- it('does not clear error message on click', () => {
- spyOn(vm, 'setErrorMessage');
-
- vm.$el.click();
-
- expect(vm.setErrorMessage).not.toHaveBeenCalled();
- });
-
- it('dispatches action', done => {
- vm.$el.querySelector('.flash-action').click();
-
- vm.$nextTick(() => {
- expect(actionSpy).toHaveBeenCalledWith('testActionPayload');
-
- done();
- });
- });
-
- it('does not dispatch action when already loading', () => {
- vm.isLoading = true;
-
- vm.$el.querySelector('.flash-action').click();
-
- expect(actionSpy).not.toHaveBeenCalledWith();
- });
-
- it('resets isLoading after click', done => {
- vm.$el.querySelector('.flash-action').click();
-
- expect(vm.isLoading).toBe(true);
-
- setTimeout(() => {
- expect(vm.isLoading).toBe(false);
-
- done();
- });
- });
-
- it('shows loading icon when isLoading is true', done => {
- expect(vm.$el.querySelector('.loading-container').style.display).not.toBe('');
-
- vm.isLoading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container').style.display).toBe('');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
index 2e1863cff86..ab28190ae64 100644
--- a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
@@ -83,8 +83,8 @@ describe('Issuable Time Tracker', () => {
initTimeTrackingComponent({
timeEstimate: 100000, // 1d 3h
timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
+ timeEstimateHumanReadable: '1d 3h',
+ timeSpentHumanReadable: '1h 23m',
});
});
@@ -98,6 +98,16 @@ describe('Issuable Time Tracker', () => {
});
});
+ it('should show full times when the sidebar is collapsed', done => {
+ Vue.nextTick(() => {
+ const timeTrackingText = vm.$el.querySelector('.time-tracking-collapsed-summary span')
+ .innerText;
+
+ expect(timeTrackingText).toBe('1h 23m / 1d 3h');
+ done();
+ });
+ });
+
describe('Remaining meter', () => {
it('should display the remaining meter with the correct width', done => {
Vue.nextTick(() => {
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 7e2d70d6eb5..acdbf064a73 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -276,6 +276,11 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz')
expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz')
expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss')
+ assert_routing({ path: "/gitlab/gitlabhq/refs/stable/logs_tree/new%0A%0Aline.txt",
+ method: :get },
+ { controller: 'projects/refs', action: 'logs_tree',
+ namespace_id: 'gitlab', project_id: 'gitlabhq',
+ id: "stable", path: "new\n\nline.txt" })
end
end
diff --git a/spec/services/boards/lists/update_service_spec.rb b/spec/services/boards/lists/update_service_spec.rb
index f28bbab941a..a5411a2fb3a 100644
--- a/spec/services/boards/lists/update_service_spec.rb
+++ b/spec/services/boards/lists/update_service_spec.rb
@@ -10,9 +10,8 @@ describe Boards::Lists::UpdateService do
context 'when user can admin list' do
it 'calls Lists::MoveService to update list position' do
board.parent.add_developer(user)
- service = described_class.new(board.parent, user, position: 1)
- expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, { position: 1 }).and_call_original
+ expect(Boards::Lists::MoveService).to receive(:new).with(board.parent, user, params).and_call_original
expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
service.execute(list)
@@ -21,8 +20,6 @@ describe Boards::Lists::UpdateService do
context 'when user cannot admin list' do
it 'does not call Lists::MoveService to update list position' do
- service = described_class.new(board.parent, user, position: 1)
-
expect(Boards::Lists::MoveService).not_to receive(:new)
service.execute(list)
@@ -34,7 +31,6 @@ describe Boards::Lists::UpdateService do
context 'when user can read list' do
it 'updates list preference for user' do
board.parent.add_guest(user)
- service = described_class.new(board.parent, user, collapsed: true)
service.execute(list)
@@ -44,8 +40,6 @@ describe Boards::Lists::UpdateService do
context 'when user cannot read list' do
it 'does not update list preference for user' do
- service = described_class.new(board.parent, user, collapsed: true)
-
service.execute(list)
expect(list.preferences_for(user).collapsed).to be_nil
@@ -54,35 +48,61 @@ describe Boards::Lists::UpdateService do
end
describe '#execute' do
+ let(:service) { described_class.new(board.parent, user, params) }
+
context 'when position parameter is present' do
+ let(:params) { { position: 1 } }
+
context 'for projects' do
- it_behaves_like 'moving list' do
- let(:project) { create(:project, :private) }
- let(:board) { create(:board, project: project) }
- end
+ let(:project) { create(:project, :private) }
+ let(:board) { create(:board, project: project) }
+
+ it_behaves_like 'moving list'
end
context 'for groups' do
- it_behaves_like 'moving list' do
- let(:group) { create(:group, :private) }
- let(:board) { create(:board, group: group) }
- end
+ let(:group) { create(:group, :private) }
+ let(:board) { create(:board, group: group) }
+
+ it_behaves_like 'moving list'
end
end
context 'when collapsed parameter is present' do
+ let(:params) { { collapsed: true } }
+
context 'for projects' do
- it_behaves_like 'updating list preferences' do
- let(:project) { create(:project, :private) }
- let(:board) { create(:board, project: project) }
- end
+ let(:project) { create(:project, :private) }
+ let(:board) { create(:board, project: project) }
+
+ it_behaves_like 'updating list preferences'
end
context 'for groups' do
- it_behaves_like 'updating list preferences' do
- let(:group) { create(:group, :private) }
- let(:board) { create(:board, group: group) }
- end
+ let(:project) { create(:project, :private) }
+ let(:board) { create(:board, project: project) }
+
+ it_behaves_like 'updating list preferences'
+ end
+ end
+
+ context 'when position and collapsed are both present' do
+ let(:params) { { collapsed: true, position: 1 } }
+
+ context 'for projects' do
+ let(:project) { create(:project, :private) }
+ let(:board) { create(:board, project: project) }
+
+ it_behaves_like 'moving list'
+ it_behaves_like 'updating list preferences'
+ end
+
+ context 'for groups' do
+ let(:group) { create(:group, :private) }
+ let(:board) { create(:board, group: group) }
+
+ it_behaves_like 'moving list'
+ it_behaves_like 'updating list preferences'
end
end
end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 07c541ec189..f2f31e1b7f2 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -11,10 +11,6 @@ shared_examples_for 'multiple issue boards' do
wait_for_requests
end
- it 'shows board switcher' do
- expect(page).to have_css('.boards-switcher')
- end
-
it 'shows current board name' do
page.within('.boards-switcher') do
expect(page).to have_content(board.name)