Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c792263edf
commit
b925465787
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -536,13 +536,6 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
|
|||
return reducedTime.length ? reducedTime : '0m';
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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: {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix ordering of issue board lists not being persisted
|
||||
merge_request: 17356
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(() => {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue