Commit b9254657 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c792263e
...@@ -74,6 +74,11 @@ const Api = { ...@@ -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 // Return namespaces list. Filtered by query
namespaces(query, callback) { namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
......
...@@ -48,6 +48,8 @@ export default () => { ...@@ -48,6 +48,8 @@ export default () => {
import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'), import('ee_component/analytics/cycle_analytics/components/custom_stage_form.vue'),
AddStageButton: () => AddStageButton: () =>
import('ee_component/analytics/cycle_analytics/components/add_stage_button.vue'), 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], mixins: [filterMixins, addStageMixin],
data() { data() {
......
...@@ -536,13 +536,6 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => { ...@@ -536,13 +536,6 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
return reducedTime.length ? reducedTime : '0m'; 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. * Calculates the milliseconds between now and a given date string.
* The result cannot become negative. * The result cannot become negative.
......
<script> <script>
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -41,12 +40,6 @@ export default { ...@@ -41,12 +40,6 @@ export default {
}, },
}, },
computed: { computed: {
timeSpent() {
return this.abbreviateTime(this.timeSpentHumanReadable);
},
timeEstimate() {
return this.abbreviateTime(this.timeEstimateHumanReadable);
},
divClass() { divClass() {
if (this.showComparisonState) { if (this.showComparisonState) {
return 'compare'; return 'compare';
...@@ -73,11 +66,11 @@ export default { ...@@ -73,11 +66,11 @@ export default {
}, },
text() { text() {
if (this.showComparisonState) { if (this.showComparisonState) {
return `${this.timeSpent} / ${this.timeEstimate}`; return `${this.timeSpentHumanReadable} / ${this.timeEstimateHumanReadable}`;
} else if (this.showEstimateOnlyState) { } else if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimate}`; return `-- / ${this.timeEstimateHumanReadable}`;
} else if (this.showSpentOnlyState) { } else if (this.showSpentOnlyState) {
return `${this.timeSpent} / --`; return `${this.timeSpentHumanReadable} / --`;
} else if (this.showNoTimeTrackingState) { } else if (this.showNoTimeTrackingState) {
return __('None'); return __('None');
} }
...@@ -100,11 +93,6 @@ export default { ...@@ -100,11 +93,6 @@ export default {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText; return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
}, },
}, },
methods: {
abbreviateTime(timeStr) {
return abbreviateTime(timeStr);
},
},
}; };
</script> </script>
......
...@@ -110,7 +110,10 @@ export default { ...@@ -110,7 +110,10 @@ export default {
<div class="ci-widget-container d-flex"> <div class="ci-widget-container d-flex">
<div class="ci-widget-content"> <div class="ci-widget-content">
<div class="media-body"> <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 }} {{ pipeline.details.name }}
<gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
>#{{ pipeline.id }}</gl-link >#{{ pipeline.id }}</gl-link
......
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,
});
<script> <script>
import DeprecatedModal from './deprecated_modal.vue'; import DeprecatedModal from './deprecated_modal.vue';
import { eventHub } from './recaptcha_eventhub';
export default { export default {
name: 'RecaptchaModal', name: 'RecaptchaModal',
...@@ -30,14 +31,11 @@ export default { ...@@ -30,14 +31,11 @@ export default {
}, },
mounted() { mounted() {
if (window.recaptchaDialogCallback) { eventHub.$on('submit', this.submit);
throw new Error('recaptchaDialogCallback is already defined!');
}
window.recaptchaDialogCallback = this.submit.bind(this);
}, },
beforeDestroy() { beforeDestroy() {
window.recaptchaDialogCallback = null; eventHub.$off('submit', this.submit);
}, },
methods: { methods: {
......
...@@ -88,7 +88,7 @@ module BoardsHelper ...@@ -88,7 +88,7 @@ module BoardsHelper
end end
def boards_link_text def boards_link_text
if multiple_boards_available? if current_board_parent.multiple_issue_boards_available?
s_("IssueBoards|Boards") s_("IssueBoards|Boards")
else else
s_("IssueBoards|Board") s_("IssueBoards|Board")
......
...@@ -4,10 +4,10 @@ module Boards ...@@ -4,10 +4,10 @@ module Boards
module Lists module Lists
class UpdateService < Boards::BaseService class UpdateService < Boards::BaseService
def execute(list) def execute(list)
return not_authorized if preferences? && !can_read?(list) update_preferences_result = update_preferences(list) if can_read?(list)
return not_authorized if position? && !can_admin?(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) success(list: list)
else else
error(list.errors.messages, 422) error(list.errors.messages, 422)
...@@ -32,10 +32,6 @@ module Boards ...@@ -32,10 +32,6 @@ module Boards
{ collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) } { collapsed: Gitlab::Utils.to_boolean(params[:collapsed]) }
end end
def not_authorized
error("Not authorized", 403)
end
def preferences? def preferences?
params.has_key?(:collapsed) params.has_key?(:collapsed)
end end
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') } = _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
.settings-content .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) = form_errors(@application_setting)
%fieldset %fieldset
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.issues-filters{ class: ("w-100" if type == :boards_modal) } .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 } .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 = render "shared/boards/switcher", board: board
= form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
- if params[:search].present? - if params[:search].present?
......
---
title: Fix ordering of issue board lists not being persisted
merge_request: 17356
author:
type: fixed
---
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 ...@@ -34,7 +34,7 @@ scope format: false do
# ref regex used in constraints. Regex verification now done in controller. # ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: { get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
id: /.*/, id: /.*/,
path: /.*/ path: /[^\0]*/
} }
end end
end end
......
...@@ -4547,6 +4547,42 @@ msgstr "" ...@@ -4547,6 +4547,42 @@ msgstr ""
msgid "CustomCycleAnalytics|Add a stage" msgid "CustomCycleAnalytics|Add a stage"
msgstr "" 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" msgid "Customize colors"
msgstr "" msgstr ""
...@@ -10208,9 +10244,6 @@ msgstr "" ...@@ -10208,9 +10244,6 @@ msgstr ""
msgid "New snippet" msgid "New snippet"
msgstr "" msgstr ""
msgid "New stage"
msgstr ""
msgid "New subgroup" msgid "New subgroup"
msgstr "" msgstr ""
...@@ -15698,6 +15731,9 @@ msgstr "" ...@@ -15698,6 +15731,9 @@ msgstr ""
msgid "There was an error fetching configuration for charts" msgid "There was an error fetching configuration for charts"
msgstr "" msgstr ""
msgid "There was an error fetching the form data"
msgstr ""
msgid "There was an error gathering the chart data" msgid "There was an error gathering the chart data"
msgstr "" msgstr ""
......
...@@ -6,6 +6,16 @@ module QA ...@@ -6,6 +6,16 @@ module QA
class Show < Page::Base class Show < Page::Base
include Page::Component::Note 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 view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do
element :merge_button element :merge_button
element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern
...@@ -27,12 +37,6 @@ module QA ...@@ -27,12 +37,6 @@ module QA
element :squash_checkbox element :squash_checkbox
end 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 view 'app/views/projects/merge_requests/show.html.haml' do
element :notes_tab element :notes_tab
element :diffs_tab element :diffs_tab
...@@ -111,6 +115,11 @@ module QA ...@@ -111,6 +115,11 @@ module QA
end end
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) def has_title?(title)
has_element?(:title, text: title) has_element?(:title, text: title)
end end
......
...@@ -11,6 +11,7 @@ module QA ...@@ -11,6 +11,7 @@ module QA
view 'app/views/projects/edit.html.haml' do view 'app/views/projects/edit.html.haml' do
element :advanced_settings element :advanced_settings
element :merge_request_settings
end end
view 'app/views/projects/settings/_general.html.haml' do view 'app/views/projects/settings/_general.html.haml' do
...@@ -41,6 +42,12 @@ module QA ...@@ -41,6 +42,12 @@ module QA
end end
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) def expand_visibility_project_features_permissions(&block)
expand_section(:visibility_features_permissions_content) do expand_section(:visibility_features_permissions_content) do
VisibilityFeaturesPermissions.perform(&block) VisibilityFeaturesPermissions.perform(&block)
......
...@@ -8,7 +8,6 @@ module QA ...@@ -8,7 +8,6 @@ module QA
include Common include Common
view 'app/views/projects/edit.html.haml' do view 'app/views/projects/edit.html.haml' do
element :merge_request_settings
element :save_merge_request_changes element :save_merge_request_changes
end end
...@@ -16,14 +15,18 @@ module QA ...@@ -16,14 +15,18 @@ module QA
element :radio_button_merge_ff element :radio_button_merge_ff
end end
def enable_ff_only def click_save_changes
expand_section(:merge_request_settings) do
click_element :radio_button_merge_ff
click_element :save_merge_request_changes click_element :save_merge_request_changes
end end
def enable_ff_only
click_element :radio_button_merge_ff
click_save_changes
end end
end end
end end
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 ...@@ -13,8 +13,12 @@ module QA
end end
project.visit! project.visit!
Page::Project::Menu.perform(&:click_settings) Page::Project::Menu.perform(&:go_to_general_settings)
Page::Project::Settings::MergeRequest.perform(&:enable_ff_only) 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 = Resource::MergeRequest.fabricate! do |merge_request|
merge_request.project = project merge_request.project = project
......
...@@ -162,10 +162,10 @@ describe Boards::ListsController do ...@@ -162,10 +162,10 @@ describe Boards::ListsController do
end end
context 'with unauthorized user' do 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 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
end end
......
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', () => { ...@@ -388,20 +388,6 @@ describe('prettyTime methods', () => {
expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour'); 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', () => { describe('calculateRemainingMilliseconds', () => {
......
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();
});
});
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();
});
});
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', () => { ...@@ -83,8 +83,8 @@ describe('Issuable Time Tracker', () => {
initTimeTrackingComponent({ initTimeTrackingComponent({
timeEstimate: 100000, // 1d 3h timeEstimate: 100000, // 1d 3h
timeSpent: 5000, // 1h 23m timeSpent: 5000, // 1h 23m
timeEstimateHumanReadable: '', timeEstimateHumanReadable: '1d 3h',
timeSpentHumanReadable: '', timeSpentHumanReadable: '1h 23m',
}); });
}); });
...@@ -98,6 +98,16 @@ describe('Issuable Time Tracker', () => { ...@@ -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', () => { describe('Remaining meter', () => {
it('should display the remaining meter with the correct width', done => { it('should display the remaining meter with the correct width', done => {
Vue.nextTick(() => { Vue.nextTick(() => {
......
...@@ -276,6 +276,11 @@ describe 'project routing' do ...@@ -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%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/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') 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
end end
......
...@@ -10,9 +10,8 @@ describe Boards::Lists::UpdateService do ...@@ -10,9 +10,8 @@ describe Boards::Lists::UpdateService do
context 'when user can admin list' do context 'when user can admin list' do
it 'calls Lists::MoveService to update list position' do it 'calls Lists::MoveService to update list position' do
board.parent.add_developer(user) 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) expect_any_instance_of(Boards::Lists::MoveService).to receive(:execute).with(list)
service.execute(list) service.execute(list)
...@@ -21,8 +20,6 @@ describe Boards::Lists::UpdateService do ...@@ -21,8 +20,6 @@ describe Boards::Lists::UpdateService do
context 'when user cannot admin list' do context 'when user cannot admin list' do
it 'does not call Lists::MoveService to update list position' 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) expect(Boards::Lists::MoveService).not_to receive(:new)
service.execute(list) service.execute(list)
...@@ -34,7 +31,6 @@ describe Boards::Lists::UpdateService do ...@@ -34,7 +31,6 @@ describe Boards::Lists::UpdateService do
context 'when user can read list' do context 'when user can read list' do
it 'updates list preference for user' do it 'updates list preference for user' do
board.parent.add_guest(user) board.parent.add_guest(user)
service = described_class.new(board.parent, user, collapsed: true)
service.execute(list) service.execute(list)
...@@ -44,8 +40,6 @@ describe Boards::Lists::UpdateService do ...@@ -44,8 +40,6 @@ describe Boards::Lists::UpdateService do
context 'when user cannot read list' do context 'when user cannot read list' do
it 'does not update list preference for user' do it 'does not update list preference for user' do
service = described_class.new(board.parent, user, collapsed: true)
service.execute(list) service.execute(list)
expect(list.preferences_for(user).collapsed).to be_nil expect(list.preferences_for(user).collapsed).to be_nil
...@@ -54,35 +48,61 @@ describe Boards::Lists::UpdateService do ...@@ -54,35 +48,61 @@ describe Boards::Lists::UpdateService do
end end
describe '#execute' do describe '#execute' do
let(:service) { described_class.new(board.parent, user, params) }
context 'when position parameter is present' do context 'when position parameter is present' do
let(:params) { { position: 1 } }
context 'for projects' do context 'for projects' do
it_behaves_like 'moving list' do
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
end
it_behaves_like 'moving list'
end end
context 'for groups' do context 'for groups' do
it_behaves_like 'moving list' do
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
let(:board) { create(:board, group: group) } let(:board) { create(:board, group: group) }
end
it_behaves_like 'moving list'
end end
end end
context 'when collapsed parameter is present' do context 'when collapsed parameter is present' do
let(:params) { { collapsed: true } }
context 'for projects' do context 'for projects' do
it_behaves_like 'updating list preferences' do
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
it_behaves_like 'updating list preferences'
end end
context 'for groups' do
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 end
context 'for groups' do context 'for groups' do
it_behaves_like 'updating list preferences' do
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
let(:board) { create(:board, group: group) } let(:board) { create(:board, group: group) }
end
it_behaves_like 'moving list'
it_behaves_like 'updating list preferences'
end end
end end
end end
......
...@@ -11,10 +11,6 @@ shared_examples_for 'multiple issue boards' do ...@@ -11,10 +11,6 @@ shared_examples_for 'multiple issue boards' do
wait_for_requests wait_for_requests
end end
it 'shows board switcher' do
expect(page).to have_css('.boards-switcher')
end
it 'shows current board name' do it 'shows current board name' do
page.within('.boards-switcher') do page.within('.boards-switcher') do
expect(page).to have_content(board.name) expect(page).to have_content(board.name)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment