Commit a3d80768 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 598e33d9 13647d24
......@@ -297,9 +297,9 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
......@@ -677,7 +677,7 @@ When your code contains more than 500 changes, any major breaking changes, or an
[license-finder-doc]: doc/development/licensing.md
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing.md
[testing]: doc/development/testing_guide/index.md
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
## GitLab Core Team & GitLab Inc. Contribution Process
## GitLab core team & GitLab Inc. contribution process
---
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import _ from 'underscore';
import Cookies from 'js-cookie';
import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
import Flash from './flash';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......
/* global Flash */
import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
function onError() {
const flash = new window.Flash('Balsamiq file could not be loaded.');
const flash = new Flash('Balsamiq file could not be loaded.');
return flash;
}
......
/* eslint-disable class-methods-use-this */
/* global Flash */
import Flash from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
......
/* global Flash */
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
export default class BlobViewer {
......
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global BoardService */
/* global Flash */
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import './models/issue';
......
......@@ -3,9 +3,9 @@
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
......
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../../../flash';
import './lists_dropdown';
const ModalStore = gl.issueBoards.ModalStore;
......
/* eslint-disable no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../../../flash';
const Store = gl.issueBoards.BoardsStore;
......
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
......
/* global Flash */
import Vue from 'vue';
import Cookies from 'js-cookie';
import Flash from '../flash';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
import stageCodeComponent from './components/stage_code_component.vue';
......
<script>
/* global Flash */
import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
......
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
const ResolveBtn = Vue.extend({
props: {
......
/* global Flash */
/* global CommentsStore */
import Vue from 'vue';
import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
window.gl = window.gl || {};
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
......
/* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownEmoji extends gl.FilteredSearchDropdown {
......
/* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
import Flash from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import './filtered_search_dropdown';
class DropdownNonUser extends gl.FilteredSearchDropdown {
......
/* global Flash */
import AjaxFilter from '~/droplab/plugins/ajax_filter';
import Flash from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
......
import Flash from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
......@@ -44,7 +45,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
new window.Flash('An error occurred while parsing recent searches');
new Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array
return [];
})
......
import AjaxCache from '../lib/utils/ajax_cache';
import '../flash'; /* global Flash */
import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
import _ from 'underscore';
window.Flash = (function() {
var hideFlash;
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
Object.assign(flashEl.style, {
transition: 'opacity .3s',
opacity: '0',
});
}
hideFlash = function() {
return $(this).fadeOut();
};
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
}, {
once: true,
passive: true,
});
/**
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to Parent element under which Flash needs to appear
* @param {Object} actionConfig Map of config to show action on banner
* @param {String} href URL to which action link should point (default '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
*/
function Flash(message, type, parent, actionConfig) {
var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
if (parent == null) {
parent = null;
}
if (parent) {
const $parent = $(parent);
this.flashContainer = $parent.find('.flash-container');
} else {
this.flashContainer = $('.flash-container-page');
}
this.flashContainer.html('');
flash = $('<div/>', {
"class": "flash-" + type
});
flash.on('click', hideFlash);
textDiv = $('<div/>', {
"class": 'flash-text',
text: message
});
textDiv.appendTo(flash);
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
};
if (actionConfig) {
const actionLinkConfig = {
class: 'flash-action',
href: actionConfig.href || '#',
text: actionConfig.title
};
const createAction = config => `
<a
href="${config.href || '#'}"
class="flash-action"
${config.href ? '' : 'role="button"'}
>
${_.escape(config.title)}
</a>
`;
if (!actionConfig.href) {
actionLinkConfig.role = 'button';
}
const createFlashEl = (message, type, isInContentWrapper = false) => `
<div
class="flash-${type}"
>
<div
class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
>
${_.escape(message)}
</div>
</div>
`;
actionLink = $('<a/>', actionLinkConfig);
/*
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
* additional action or link on banner next to message
*
* @param {String} message Flash message text
* @param {String} type Type of Flash, it can be `notice` or `alert` (default)
* @param {Object} parent Reference to parent element under which Flash needs to appear
* @param {Object} actonConfig Map of config to show action on banner
* @param {String} href URL to which action config should point to (default: '#')
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
* @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
*/
const createFlash = function createFlash(
message,
type = 'alert',
parent = document,
actionConfig = null,
fadeTransition = true,
) {
const flashContainer = parent.querySelector('.flash-container');
actionLink.appendTo(flash);
this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
}
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
if (!flashContainer) return null;
const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
if (actionConfig) {
flashEl.innerHTML += createAction(actionConfig);
if (actionConfig.clickHandler) {
flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
}
flash.appendTo(this.flashContainer);
this.flashContainer.show();
}
Flash.prototype.destroy = function() {
this.flashContainer.html('');
};
flashContainer.style.display = 'block';
return flashContainer;
};
return Flash;
})();
export {
createFlash as default,
createFlashEl,
createAction,
hideFlash,
};
window.Flash = createFlash;
......@@ -46,6 +46,10 @@ class GeoNodeStatus {
$closestStatus.toggleClass('hidden');
}
static formatCountAndPercentage(count, total, percentage) {
return `${gl.text.addDelimiter(count)}/${gl.text.addDelimiter(total)} (${percentage})`;
}
getStatus() {
$.getJSON(this.endpoint, (status) => {
this.setStatusIcon(status.healthy);
......@@ -62,10 +66,28 @@ class GeoNodeStatus {
this.$dbReplicationLag.text('UNKNOWN');
}
this.$repositoriesSynced.text(`${status.repositories_synced_count}/${status.repositories_count} (${status.repositories_synced_in_percentage})`);
this.$repositoriesFailed.text(status.repositories_failed_count);
this.$lfsObjectsSynced.text(`${status.lfs_objects_synced_count}/${status.lfs_objects_count} (${status.lfs_objects_synced_in_percentage})`);
this.$attachmentsSynced.text(`${status.attachments_synced_count}/${status.attachments_count} (${status.attachments_synced_in_percentage})`);
const repoText = GeoNodeStatus.formatCountAndPercentage(
status.repositories_synced_count,
status.repositories_count,
status.repositories_synced_in_percentage);
const repoFailedText = gl.text.addDelimiter(status.repositories_failed_count);
const lfsText = GeoNodeStatus.formatCountAndPercentage(
status.lfs_objects_synced_count,
status.lfs_objects_count,
status.lfs_objects_synced_in_percentage);
const attachmentText = GeoNodeStatus.formatCountAndPercentage(
status.attachments_synced_count,
status.attachments_count,
status.attachments_synced_in_percentage);
this.$repositoriesSynced.text(repoText);
this.$repositoriesFailed.text(repoFailedText);
this.$lfsObjectsSynced.text(lfsText);
this.$attachmentsSynced.text(attachmentText);
const eventDate = gl.utils.formatDate(new Date(status.last_event_date));
const cursorDate = gl.utils.formatDate(new Date(status.cursor_last_event_date));
this.$lastEventSeen.text(`${status.last_event_id} (${eventDate})`);
......
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
......
......@@ -2,6 +2,8 @@ document.addEventListener('DOMContentLoaded', () => {
const showGroupLink = () => {
const $cnLink = $('.cn-link');
const $filterLink = $('.filter-link');
if (!$cnLink.length || !$filterLink.length) return;
const $checkedSync = $('input[name="sync_method"]:checked').val() === 'group';
$cnLink.toggle($checkedSync);
......
/* global Flash */
import Flash from '../flash';
export default class IntegrationSettingsForm {
constructor(formSelector) {
......@@ -102,7 +102,7 @@ export default class IntegrationSettingsForm {
})
.done((res) => {
if (res.error) {
new Flash(`${res.message} ${res.service_response}`, null, null, {
new Flash(`${res.message} ${res.service_response}`, 'alert', document, {
title: 'Save anyway',
clickHandler: (e) => {
e.preventDefault();
......
<script>
/* global Flash */
/*
`rawReferences` are separated by spaces.
Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']`
......@@ -26,6 +24,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import Flash from '../../../flash';
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
......
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
/* global Flash */
import _ from 'underscore';
import Flash from './flash';
export default {
init({ container, form, issues, prefixId } = {}) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
import 'vendor/jquery.waitforimages';
import '~/lib/utils/text_utility';
import './flash';
import Flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
......
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
......@@ -153,7 +152,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error updating issue');
window.Flash('Error updating issue');
});
},
deleteIssuable() {
......@@ -167,7 +166,7 @@ export default {
})
.catch(() => {
eventHub.$emit('close.form');
return new Flash('Error deleting issue');
window.Flash('Error deleting issue');
});
},
},
......
<script>
/* global Flash */
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
......
/* global Flash */
import Vue from 'vue';
import JobMediator from './job_details_mediator';
import jobHeader from './components/header.vue';
......
/* global Flash */
/* global Build */
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import JobStore from './stores/job_store';
import JobService from './services/job_service';
......
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
/* global Sortable */
import Flash from './flash';
((global) => {
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
......
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
......@@ -57,6 +56,7 @@ import './u2f/util';
import './activities';
import './admin';
import './api';
import './ajax_loading_spinner';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
......@@ -76,7 +76,7 @@ import './diff';
import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
import Flash from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
......@@ -178,7 +178,6 @@ $(function () {
var $document = $(document);
var $window = $(window);
var $sidebarGutterToggle = $('.js-sidebar-toggle');
var $flash = $('.flash-container');
var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize;
......@@ -263,13 +262,6 @@ $(function () {
// Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
// Flash
if ($flash.length > 0) {
$flash.click(function () {
return $(this).fadeOut();
});
$flash.show();
}
// Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) {
var buttons;
......
/* eslint-disable class-methods-use-this, promise/catch-or-return */
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
(() => {
window.gl = window.gl || {};
......
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global ace */
/* global Flash */
import Vue from 'vue';
import Flash from '../../flash';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......
/* eslint-disable new-cap, comma-dangle, no-new */
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import './merge_conflict_service';
......
/* eslint-disable no-new, class-methods-use-this */
/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
import './flash';
import Flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
/* global Sortable */
import Flash from './flash';
(function() {
this.Milestone = (function() {
function Milestone() {
......
/* eslint-disable no-new */
/* global Flash */
import Flash from './flash';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
......
/* global Flash */
import Flash from '../flash';
import AUTH_METHOD from './constants';
import { backOff } from '../lib/utils/common_utils';
......
<script>
/* global Flash */
import _ from 'underscore';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import Graph from './graph.vue';
......
......@@ -5,7 +5,6 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
/* global Flash */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
......@@ -18,6 +17,7 @@ import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave';
......@@ -354,7 +354,7 @@ export default class Notes {
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
this.refresh();
}
return;
......@@ -593,7 +593,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline);
return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
}
updateNoteError($parentTimeline) {
......@@ -1213,13 +1213,13 @@ export default class Notes {
}
addFlash(...flashParams) {
this.flashInstance = new Flash(...flashParams);
this.flashContainer = new Flash(...flashParams);
}
clearFlash() {
if (this.flashInstance && this.flashInstance.flashContainer) {
this.flashInstance.flashContainer.hide();
this.flashInstance = null;
if (this.flashContainer) {
this.flashContainer.style.display = 'none';
this.flashContainer = null;
}
}
......
<script>
/* global Flash, Autosave */
/* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
import Flash from '../../flash';
import '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
......@@ -145,7 +146,7 @@
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
$(this.$refs.commentForm),
this.$refs.commentForm,
);
}
} else {
......@@ -160,7 +161,7 @@
this.isSubmitting = false;
this.discard(false);
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
......
<script>
/* global Flash */
import { mapActions, mapGetters } from 'vuex';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
......@@ -133,7 +133,7 @@
this.isReplying = true;
this.$nextTick(() => {
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.$refs.noteForm.note = noteText;
callback(err);
});
......
<script>
/* global Flash */
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issueNoteHeader from './issue_note_header.vue';
import issueNoteActions from './issue_note_actions.vue';
......@@ -101,7 +100,7 @@
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', $(this.$el));
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
......
<script>
/* global Flash */
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
......
<script>
/* global Flash */
import { mapGetters, mapActions } from 'vuex';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import issueNote from './issue_note.vue';
......
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
......@@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
Flash('Commands applied', 'notice', $(noteData.flashContainer));
Flash('Commands applied', 'notice', noteData.flashContainer);
}
if (commandsChanges) {
......@@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
.catch(() => {
Flash(
'Something went wrong while adding your award. Please try again.',
null,
$(noteData.flashContainer),
'alert',
noteData.flashContainer,
);
});
}
......@@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}
if (errors && errors.commands_only) {
Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
Flash(errors.commands_only, 'notice', noteData.flashContainer);
}
commit(types.REMOVE_PLACEHOLDER_NOTES);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
/* global Flash */
import Flash from './flash';
(function() {
this.NotificationsDropdown = (function() {
......
<script>
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
......@@ -13,7 +13,7 @@
* 4. Commit widget
*/
/* global Flash */
import Flash from '../../flash';
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
......
/* global Flash */
import '~/flash';
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue';
......
/* global Flash */
import Vue from 'vue';
import Flash from '../flash';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
......
/* global Flash */
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
......
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
/* global Flash */
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
((global) => {
......
<script>
/* global Flash */
import Flash from '../../../flash';
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
......@@ -44,7 +43,7 @@ export default {
methods: {
fetchIncomingEmail() {
if (this.flash) {
this.flash.destroy();
this.flash.innerHTML = '';
}
this.service.fetchIncomingEmail()
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '../flash';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
......@@ -57,7 +56,7 @@ export default class ProtectedBranchEdit {
},
},
error() {
new Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
},
}).always(() => {
this.$allowedToMergeDropdown.enable();
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '../flash';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
......@@ -43,7 +42,7 @@ export default class ProtectedTagEdit {
},
},
error() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
......
<script>
/* global Flash */
import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
......
/* global Flash */
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import '../../flash';
import Flash from '../../flash';
const RepoHelper = {
monacoInstance: null,
......
/* global Flash */
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
......
/* global Flash */
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Flash */
import Flash from './flash';
import Api from './api';
(function() {
......
/* global Flash */
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
......
<script>
/* global Flash */
import Flash from '../../../flash';
import editForm from './edit_form.vue';
export default {
......
/* global Flash */
function isValidProjectId(id) {
return id > 0;
}
......@@ -38,7 +36,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
.catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
......@@ -73,7 +71,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
Flash('An error occurred while moving the issue.');
window.Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
......
/* global Flash */
import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
import Flash from './flash';
import { __, s__ } from './locale';
export default class Star {
......
/* global Flash */
import 'deckar01-task_list';
import Flash from './flash';
export default class TaskList {
constructor(options = {}) {
......
/* global Flash */
import '~/lib/utils/datetime_utility';
import Flash from '../../flash';
import MemoryUsage from './mr_widget_memory_usage';
import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
......
/* global Flash */
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
......
/* global Flash */
import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
......
/* global Flash */
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
......
/* global Flash */
import statusIcon from '../mr_widget_status_icon';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
......@@ -27,12 +26,12 @@ export default {
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
......
/* global Flash */
import Flash from '../flash';
import {
WidgetHeader,
WidgetMergeHelp,
......
<script>
/* global Flash */
import Flash from '../../../flash';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
......
......@@ -17,17 +17,11 @@
/* Trial Banner */
.gitlab-ee-license-banner.alert {
display: flex;
align-content: center;
justify-content: center;
padding: 15px 35px;
position: fixed;
top: 0;
width: 100%;
position: relative;
background: $red-400;
color: $white-light;
text-align: center;
z-index: 400;
@media (max-width: $screen-md-min) {
padding: 10px 35px;
......@@ -54,8 +48,8 @@
.close {
position: absolute;
top: 15%;
right: 10px;
top: 12px;
right: $gl-padding;
}
}
......
......@@ -43,6 +43,9 @@ class Groups::LdapGroupLinksController < Groups::ApplicationController
end
def ldap_group_link_params
params.require(:ldap_group_link).permit(:cn, :filter, :group_access, :provider)
attrs = %i[cn group_access provider]
attrs << :filter if ::License.feature_available?(:ldap_group_sync_filter)
params.require(:ldap_group_link).permit(attrs)
end
end
......@@ -17,6 +17,6 @@ class Groups::LdapsController < Groups::ApplicationController
private
def check_enabled_extras!
render_404 unless Gitlab::LDAP::Config.enabled_extras?
render_404 unless Gitlab::LDAP::Config.group_sync_enabled?
end
end
......@@ -230,20 +230,12 @@ class Group < Namespace
ldap_group_links.first.try(:cn)
end
def ldap_filter
ldap_group_links.first.try(:filter)
end
def ldap_access
ldap_group_links.first.try(:group_access)
end
def ldap_cn_or_filter_present?
ldap_cn.present? || ldap_filter.present?
end
def ldap_synced?
Gitlab.config.ldap.enabled && ldap_cn_or_filter_present?
Gitlab.config.ldap.enabled && ldap_group_links.any?(&:active?)
end
def post_create_hook
......
......@@ -43,6 +43,14 @@ class LdapGroupLink < ActiveRecord::Base
config.label
end
def active?
if filter.present?
::License.feature_available?(:ldap_group_sync_filter)
elsif cn.present?
::License.feature_available?(:ldap_group_sync)
end
end
private
def nullify_blank_attributes
......
......@@ -18,10 +18,11 @@ class License < ActiveRecord::Base
issue_board_milestone
issue_weights
jenkins_integration
ldap_extras
ldap_group_sync
merge_request_approvers
merge_request_rebase
merge_request_squash
multiple_ldap_servers
multiple_issue_assignees
multiple_issue_boards
push_rules
......@@ -42,6 +43,7 @@ class License < ActiveRecord::Base
geo
group_issue_boards
jira_dev_panel_integration
ldap_group_sync_filter
object_storage
service_desk
variable_environment_scope
......@@ -108,7 +110,9 @@ class License < ActiveRecord::Base
elastic_search
extended_audit_events
geo
ldap_extras
ldap_group_sync
ldap_group_sync_filter
multiple_ldap_servers
object_storage
repository_size_limit
].freeze
......
......@@ -36,7 +36,7 @@
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
- if ldap_enabled? && @group.persisted?
- if @group.persisted? && Gitlab::LDAP::Config.group_sync_enabled?
%h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group
......@@ -62,11 +62,11 @@
= render partial: "namespaces/shared_runner_status", locals: { namespace: @group }
.panel.panel-default
.panel-heading Active synchronizations
%ul.well-list
- if @group.ldap_group_links.any?
- @group.ldap_group_links.each do |ldap_group_link|
- if Gitlab::LDAP::Config.group_sync_enabled? && @group.ldap_synced?
.panel.panel-default
.panel-heading Active synchronizations
%ul.well-list
- @group.ldap_group_links.select(&:active?).each do |ldap_group_link|
%li
%strong= ldap_group_link.cn ? "Group: #{ldap_group_link.cn}" : "Filter: #{truncate(ldap_group_link.filter, length: 40)}"
as
......
......@@ -3,7 +3,7 @@
The members of this group are managed using LDAP and cannot be added or removed here.
Because LDAP permissions in GitLab get updated one user at a time and because GitLab caches LDAP check results, changes on your LDAP server or in this group's LDAP sync settings may take up to #{Gitlab.config.ldap['sync_time']}s to show in the list below.
%ul
- @group.ldap_group_links.each do |ldap_group_link|
- @group.ldap_group_links.select(&:active?).each do |ldap_group_link|
%li
People in
%code= ldap_group_link.cn ? "cn: #{ldap_group_link.cn}" : "filter: #{truncate(ldap_group_link.filter, length: 70)}"
......
- page_title 'LDAP Syncrhonizations'
- page_title 'LDAP Synchronization'
%h3.page-title LDAP synchronizations
= render 'ldap_group_links/form', group: @group
= render 'ldap_group_links/ldap_group_links', group: @group
......@@ -4,6 +4,7 @@
.content-wrapper.page-with-new-nav
.mobile-overlay
.alert-wrapper
= render "layouts/header/ee_license_banner"
= render "layouts/broadcast"
= yield :flash_message
- unless @hide_breadcrumbs
......
= render "layouts/header/ee_license_banner"
%header.navbar.navbar-gitlab
%header.navbar.navbar-gitlab.navbar-gitlab-new
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
......
......@@ -9,18 +9,19 @@
.col-sm-10
= f.select :provider, ldap_server_select_options, {}, class: 'form-control'
.form-group.row
= f.label :cn, class: 'control-label col-sm-2' do
Sync method
.col-sm-10
.radio
= label_tag :sync_method_group do
= radio_button_tag :sync_method, :group, true
LDAP Group cn
.radio
= label_tag :sync_method_filter do
= radio_button_tag :sync_method, :filter
LDAP user filter
- if ::License.feature_available?(:ldap_group_sync_filter)
.form-group.row
= f.label :cn, class: 'control-label col-sm-2' do
Sync method
.col-sm-10
.radio
= label_tag :sync_method_group do
= radio_button_tag :sync_method, :group, true
LDAP Group cn
.radio
= label_tag :sync_method_filter do
= radio_button_tag :sync_method, :filter
LDAP user filter
.form-group.row.cn-link
= f.label :cn, class: 'control-label col-sm-2' do
......@@ -32,16 +33,17 @@
%br
If you select an LDAP group you do not belong to you will lose ownership of #{group.name}.
.form-group.row.filter-link
= f.label :filter, class: 'control-label col-sm-2' do
LDAP User filter
.col-sm-10
= f.text_field :filter, placeholder: 'Ex. (&(objectCategory=person)(objectClass=developer))', class: 'form-control xxlarge input-mn-300'
.help-block
- ldap_link = link_to 'LDAP Search Filter Syntax', 'https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx'
This query must use valid #{ldap_link}. Synchronize #{group.name}'s members with this LDAP user filter.
%br
If you do not belong to this LDAP user filter you will lose ownership of #{group.name}.
- if ::License.feature_available?(:ldap_group_sync_filter)
.form-group.row.filter-link
= f.label :filter, class: 'control-label col-sm-2' do
LDAP User filter
.col-sm-10
= f.text_field :filter, placeholder: 'Ex. (&(objectCategory=person)(objectClass=developer))', class: 'form-control xxlarge input-mn-300'
.help-block
- ldap_link = link_to 'LDAP Search Filter Syntax', 'https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx'
This query must use valid #{ldap_link}. Synchronize #{group.name}'s members with this LDAP user filter.
%br
If you do not belong to this LDAP user filter you will lose ownership of #{group.name}.
.form-group.row
= f.label :group_access, class: 'control-label col-sm-2' do
......
......@@ -13,3 +13,6 @@
Config for LDAP server
%code= ldap_group_link.provider
is not present in GitLab
- unless ldap_group_link.active?
(Inactive because syncing with an LDAP user filter is not included in the current license)
......@@ -3,7 +3,7 @@ class LdapAllGroupsSyncWorker
include CronjobQueue
def perform
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
logger.info 'Started LDAP group sync'
EE::Gitlab::LDAP::Sync::Groups.execute
......
......@@ -3,7 +3,7 @@ class LdapGroupSyncWorker
include DedicatedSidekiqQueue
def perform(group_ids, provider = nil)
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
groups = Group.where(id: Array(group_ids))
......
......@@ -3,7 +3,7 @@ class LdapSyncWorker
include CronjobQueue
def perform
return unless Gitlab::LDAP::Config.enabled_extras?
return unless Gitlab::LDAP::Config.group_sync_enabled?
Rails.logger.info "Performing daily LDAP sync task."
User.ldap.find_each(batch_size: 100).each do |ldap_user|
......
---
title: Fix base link for issues on group boards
merge_request:
author:
type: fixed
......@@ -20,7 +20,7 @@ membership syncing.
## Use-cases
- User Sync: Once a day, GitLab will update users against LDAP
- Group Sync: Once an hour, GitLab will update group membership
- Group Sync: Once an hour, GitLab will update group membership
based on LDAP group members
## User Sync
......@@ -54,8 +54,8 @@ new groups they might be added to when the user logs in. That way they don't nee
to wait for the hourly sync to be granted access to the groups that they are in
in LDAP.
We can also add a GitLab group to sync with one or multiple LDAP groups or we can
also add a filter. The filter must comply with the syntax defined in [RFC 2254](https://tools.ietf.org/search/rfc2254).
In GitLab Enterprise Edition Premium, we can also add a GitLab group to sync with one or multiple LDAP groups or we can
also add a filter. The filter must comply with the syntax defined in [RFC 2254](https://tools.ietf.org/search/rfc2254).
A group sync process will run every hour on the hour, and `group_base` must be set
in LDAP configuration for LDAP synchronizations based on group CN to work. This allows
......
# Development
# GitLab development guides
## Outside of docs
## Get started!
- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide
- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process
- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version
## Styleguides
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
contributing to documentation.
- [Writing documentation](writing_documentation.md)
- [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [`Gemfile` guidelines](gemfile.md)
- Setup GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
- [GitLab contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
- [Architecture](architecture.md) of GitLab
- [Rake tasks](rake_tasks.md) for development
## Process
## Processes
- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md)
- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
- [Guidelines for implementing Enterprise Edition feature](ee_features.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
## Backend howtos
## UX and frontend guides
- [Architecture](architecture.md) of GitLab
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
## Backend guides
- [Testing standards and style guidelines](testing_guide/index.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API.
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags.md)
- [View sent emails or preview mailers](emails.md)
- [Shell commands](shell_commands.md) in the GitLab codebase
- [`Gemfile` guidelines](gemfile.md)
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
## Performance guides
- [Instrumentation](instrumentation.md)
- [Performance guidelines](performance.md)
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
- [Object state models](object_state_models.md)
- [Building a package for testing purposes](build_test_package.md)
- [Manage feature flags](feature_flags.md)
- [View sent emails or preview mailers](emails.md)
- [Working with Gitaly](gitaly.md)
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
## Databases
## Databases guides
### Migrations
- [Merge Request Checklist](database_merge_request_checklist.md)
- [What requires downtime?](what_requires_downtime.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Migrations style guide](migration_style_guide.md) for creating safe SQL migrations
- [Post deployment migrations](post_deployment_migrations.md)
- [Background migrations](background_migrations.md)
- [Swapping tables](swapping_tables.md)
### Best practices
- [Merge Request checklist](database_merge_request_checklist.md)
- [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign Keys & Associations](foreign_keys.md)
- [Serializing Data](serializing_data.md)
- [Polymorphic Associations](polymorphic_associations.md)
- [Single Table Inheritance](single_table_inheritance.md)
- [Foreign keys & associations](foreign_keys.md)
- [Serializing data](serializing_data.md)
- [Polymorphic associations](polymorphic_associations.md)
- [Single table inheritance](single_table_inheritance.md)
- [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
- [Iterating Tables In Batches](iterating_tables_in_batches.md)
- [Ordering Table Columns](ordering_table_columns.md)
- [Verifying Database Capabilities](verifying_database_capabilities.md)
- [Hash Indexes](hash_indexes.md)
- [Storing SHA1 hashes as binary](sha1_as_binary.md)
- [Iterating tables in batches](iterating_tables_in_batches.md)
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Hash indexes](hash_indexes.md)
- [Swapping Tables](swapping_tables.md)
## Internationalization (i18n)
## Documentation guides
- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are
contributing to the documentation.
- [Writing documentation](writing_documentation.md)
- [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
## Internationalization (i18n) guides
- [Introduction](i18n/index.md)
- [Externalization](i18n/externalization.md)
- [Translation](i18n/translation.md)
## Build guides
- [Building a package for testing purposes](build_test_package.md)
## Compliance
- [Licensing](licensing.md) for ensuring license compliance
......@@ -54,7 +54,8 @@ or make changes to our frontend development guidelines.
---
## [Testing](testing.md)
## [Testing](../testing_guide/frontend_testing.md)
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
......
# Frontend Testing
There are two types of test suites you'll encounter while developing frontend code
at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec
feature tests with Capybara for e2e (end-to-end) integration testing.
Unit and feature tests need to be written for all new features.
Most of the time, you should use rspec for your feature tests.
There are cases where the behaviour you are testing is not worth the time spent running the full application,
for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend,
you should write an integration test using Jasmine.
![Testing priority triangle](img/testing_triangle.png)
_This diagram demonstrates the relative priority of each test type we use_
Regression tests should be written for bug fixes to prevent them from recurring in the future.
See [the Testing Standards and Style Guidelines](../testing.md)
for more information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
framework for our JavaScript unit and integration tests. For integration tests,
we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
The existing static fixtures will be migrated over time.
Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
JavaScript tests live in `spec/javascripts/`, matching the folder structure
of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
Keep in mind that in a CI environment, these tests are run in a headless
browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
### Best practice
#### Naming unit tests
When writing describe test blocks to test specific functions/methods,
please use the method name as the describe block name.
```javascript
// Good
describe('methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
// Bad
describe('#methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
// Bad
describe('.methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
```
#### Testing Promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred.
```javascript
// Good
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(done.fail);
});
// Good
it('tests a promise rejection', (done) => {
promise
.then(done.fail)
.catch((error) => {
expect(error).toBe(expectedError);
})
.then(done)
.catch(done.fail);
});
// Bad (missing done callback)
it('tests a promise', () => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
});
// Bad (missing catch)
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
});
// Bad (use done.fail in asynchronous tests)
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(fail)
});
// Bad (missing catch)
it('tests a promise rejection', (done) => {
promise
.catch((error) => {
expect(error).toBe(expectedError);
})
.then(done)
});
```
#### Stubbing
For unit tests, you should stub methods that are unrelated to the current unit you are testing.
If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
For integration tests, you should stub methods that will effect the stability of the test if they
execute their original behaviour. i.e. Network requests.
### Vue.js unit tests
See this [section][vue-test].
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
### Live testing and focused testing
While developing locally, it may be helpful to keep karma running so that you
can get instant feedback on as you write tests and modify code. To do this
you can start karma with `npm run karma-start`. It will compile the javascript
assets and run a server at `http://localhost:9876/` where it will automatically
run the tests on any browser which connects to it. You can enter that url on
multiple browsers at once to have it run the tests on each in parallel.
While karma is running, any changes you make will instantly trigger a recompile
and retest of the entire test suite, so you can see instantly if you've broken
a test with your changes. You can use [jasmine focused][jasmine-focus] or
excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
## RSpec Feature Integration Tests
Information on setting up and running RSpec integration tests with
[Capybara][capybara] can be found in the
[general testing guide](../testing.md).
## Gotchas
### Errors due to use of unsupported JavaScript features
Similar errors will be thrown if you're using JavaScript features not yet
supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
- Array.first
- Async functions
- Generators
- Array destructuring
- For..Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
### RSpec errors due to JavaScript
By default RSpec unit tests will not run JavaScript in the headless browser
and will simply rely on inspecting the HTML generated by rails.
If an integration test depends on JavaScript to run correctly, you need to make
sure the spec is configured to enable JavaScript when the tests are run. If you
don't do this you'll see vague error messages from the spec runner.
To enable a JavaScript driver in an `rspec` test, add `:js` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', :js do
# assertions...
end
describe "Admin::AbuseReports", :js do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
### Spinach errors due to missing JavaScript
> **Note:** Since we are discouraging the use of Spinach when writing new
> feature tests, you shouldn't ever need to use this. This information is kept
> available for legacy purposes only.
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
[capybara]: http://teamcapybara.github.io/capybara/
[jasmine]: https://jasmine.github.io/
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
This document was moved to [../testing_guide/frontend_testing.md](../testing_guide/frontend_testing.md).
# Testing Standards and Style Guidelines
This guide outlines standards and best practices for automated testing of GitLab
CE and EE.
It is meant to be an _extension_ of the [thoughtbot testing
styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
this guide defines a rule that contradicts the thoughtbot guide, this guide
takes precedence. Some guidelines may be repeated verbatim to stress their
importance.
## Definitions
### Unit tests
Formal definition: https://en.wikipedia.org/wiki/Unit_testing
These kind of tests ensure that a single unit of code (a method) works as
expected (given an input, it has a predictable output). These tests should be
isolated as much as possible. For example, model methods that don't do anything
with the database shouldn't need a DB record. Classes that don't need database
records should use stubs/doubles as much as possible.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
| `app/finders/` | `spec/finders/` | RSpec | |
| `app/helpers/` | `spec/helpers/` | RSpec | |
| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
| `app/policies/` | `spec/policies/` | RSpec | |
| `app/presenters/` | `spec/presenters/` | RSpec | |
| `app/routing/` | `spec/routing/` | RSpec | |
| `app/serializers/` | `spec/serializers/` | RSpec | |
| `app/services/` | `spec/services/` | RSpec | |
| `app/tasks/` | `spec/tasks/` | RSpec | |
| `app/uploaders/` | `spec/uploaders/` | RSpec | |
| `app/views/` | `spec/views/` | RSpec | |
| `app/workers/` | `spec/workers/` | RSpec | |
| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
### Integration tests
Formal definition: https://en.wikipedia.org/wiki/Integration_testing
These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
| `app/controllers/` | `spec/controllers/` | RSpec | |
| `app/mailers/` | `spec/mailers/` | RSpec | |
| `lib/api/` | `spec/requests/api/` | RSpec | |
| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
#### About controller tests
In an ideal world, controllers should be thin. However, when this is not the
case, it's acceptable to write a system/feature test without JavaScript instead
of a controller test. The reason is that testing a fat controller usually
involves a lot of stubbing, things like:
```ruby
controller.instance_variable_set(:@user, user)
```
and use methods which are deprecated in Rails 5 ([#23768]).
[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
#### About Karma
As you may have noticed, Karma is both in the Unit tests and the Integration
tests category. That's because Karma is a tool that provides an environment to
run JavaScript tests, so you can either run unit tests (e.g. test a single
JavaScript method), or integration tests (e.g. test a component that is composed
of multiple components).
### System tests or Feature tests
Formal definition: https://en.wikipedia.org/wiki/System_testing.
These kind of tests ensure the application works as expected from a user point
of view (aka black-box testing). These tests should test a happy path for a
given page or set of pages, and a test case should be added for any regression
that couldn't have been caught at lower levels with better tests (i.e. if a
regression is found, regression tests should be added at the lowest-level
possible).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
[Capybara]: https://github.com/teamcapybara/capybara
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
[RackTest]: https://github.com/teamcapybara/capybara#racktest
#### Best practices
- Create only the necessary records in the database
- Test a happy path and a less happy path but that's it
- Every other possible path should be tested with Unit or Integration tests
- Test what's displayed on the page, not the internals of ActiveRecord models.
For instance, if you want to verify that a record was created, add
expectations that its attributes are displayed on the page, not that
`Model.count` increased by one.
- It's ok to look for DOM elements but don't abuse it since it makes the tests
more brittle
If we're confident that the low-level components work well (and we should be if
we have enough Unit & Integration tests), we shouldn't need to duplicate their
thorough testing at the System test level.
It's very easy to add tests, but a lot harder to remove or improve tests, so one
should take care of not introducing too many (slow and duplicated) specs.
The reasons why we should follow these best practices are as follows:
- System tests are slow to run since they spin up the entire application stack
in a headless browser, and even slower when they integrate a JS driver
- When system tests run with a JavaScript driver, the tests are run in a
different thread than the application. This means it does not share a
database connection and your test will have to commit the transactions in
order for the running application to see the data (and vice-versa). In that
case we need to truncate the database after each spec instead of simply
rolling back a transaction (the faster strategy that's in use for other kind
of tests). This is slower than transactions, however, so we want to use
truncation only when necessary.
### Black-box tests or End-to-end tests
GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
are configured and packaged by [GitLab Omnibus].
[GitLab QA] is a tool that allows to test that all these pieces integrate well
together by building a Docker image for a given version of GitLab Rails and
running feature tests (i.e. using Capybara) against it.
The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase.
[multiple pieces]: ./architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[Gitaly]: https://gitlab.com/gitlab-org/gitaly
[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
### EE-specific tests
EE-specific tests follows the same organization, but under the `spec/ee` folder.
## Test for what should not be there
This is particularly important for permission calls and might be called a
negative assertion: make sure only the bare minimum is returned and nothing else.
See an issue about [leaking tokens] as an example of a vulnerability that is
captured by such a test.
[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948
## How to test at the correct level?
As many things in life, deciding what to test at each level of testing is a
trade-off:
- Unit tests are usually cheap, and you should consider them like the basement
of your house: you need them to be confident that your code is behaving
correctly. However if you run only unit tests without integration / system
tests, you might [miss] the [big] [picture]!
- Integration tests are a bit more expensive, but don't abuse them. A system test
is often better than an integration test that is stubbing a lot of internals.
- System tests are expensive (compared to unit tests), even more if they require
a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
section.
Another way to see it is to think about the "cost of tests", this is well
explained [in this article][tests-cost] and the basic idea is that the cost of a
test includes:
- The time it takes to write the test
- The time it takes to run the test every time the suite runs
- The time it takes to understand the test
- The time it takes to fix the test if it breaks and the underlying code is OK
- Maybe, the time it takes to change the code to make the code testable.
[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
[big]: https://twitter.com/timbray/status/822470746773409794
[picture]: https://twitter.com/withzombies/status/829716565834752000
[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
## Frontend testing
Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
## RSpec
### General Guidelines
- Use a single, top-level `describe ClassName` block.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
### Automatic retries and flaky tests detection
On our CI, we use [rspec-retry] to automatically retry a failing example a few
times (see [`spec/spec_helper.rb`] for the precise retries count).
We also use a home-made `RspecFlaky::Listener` listener which records flaky
examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
is detected in any other branch (`flaky-examples-check` job). In the future, the
`flaky-examples-check` job will not be allowed to fail.
[rspec-retry]: https://github.com/NoRedInk/rspec-retry
[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
duplication. However, this sometimes [comes at the cost of clarity][lets-not],
so we need to set some guidelines for their use going forward:
- `let` variables are preferable to instance variables. Local variables are
preferable to `let` variables.
- Use `let` to reduce duplication throughout an entire spec file.
- Don't use `let` to define variables used by a single test; define them as
local variables inside the test's `it` block.
- Don't define a `let` variable inside the top-level `describe` block that's
only used in a more deeply-nested `context` or `describe` block. Keep the
definition as close as possible to where it's used.
- Try to avoid overriding the definition of one `let` variable with another.
- Don't define a `let` variable that's only used by the definition of another.
Use a helper method instead.
[lets-not]: https://robots.thoughtbot.com/lets-not
#### `set` variables
In some cases there is no need to recreate the same object for tests again for
each example. For example, a project is needed to test issues on the same
project, one project will do for the entire file. This can be achieved by using
`set` in the same way you would use `let`.
`rspec-set` only works on ActiveRecord objects, and before new examples it
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
There is one gotcha; you can't reference a model defined in a `let` block in a
`set` block.
### Time-sensitive tests
[Timecop](https://github.com/travisjeffery/timecop) is available in our
Ruby-based tests for verifying things that are time-sensitive. Any test that
exercises or verifies something time-sensitive should make use of Timecop to
prevent transient test failures.
Example:
```ruby
it 'is overdue' do
issue = build(:issue, due_date: Date.tomorrow)
Timecop.freeze(3.days.from_now) do
expect(issue).to be_overdue
end
end
```
### System / Feature tests
- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
`user_changes_password_spec.rb`.
- Use only one `feature` block per feature spec file.
- Use scenario titles that describe the success and failure paths.
- Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title.
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive
range of inputs. By specifying the test case once, alongside a table of inputs
and the expected output for each, your tests can be made easier to read and more
compact.
We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
gem. A short example, using the table syntax and checking Ruby equality for a
range of inputs, might look like this:
```ruby
describe "#==" do
using RSpec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
project1 | project1 | true
project2 | project2 | true
project 1 | project2 | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
```
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
complexity of RSpec expectations.They should be placed under
`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
they apply to multiple type of specs.
#### have_gitlab_http_status
Prefer `have_gitlab_http_status` over `have_http_status` because the former
could also show the response body whenever the status mismatched. This would
be very useful whenever some tests start breaking and we would love to know
why without editing the source and rerun the tests.
This is especially useful whenever it's showing 500 internal server error.
### Shared contexts
All shared contexts should be be placed under `spec/support/shared_contexts/`.
Shared contexts can be placed in subfolder if they apply to a certain type of
specs only (e.g. features, requests etc.) but shouldn't be if they apply to
multiple type of specs.
Each file should include only one context and have a descriptive name, e.g.
`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
### Shared examples
All shared examples should be be placed under `spec/support/shared_examples/`.
Shared examples can be placed in subfolder if they apply to a certain type of
specs only (e.g. features, requests etc.) but shouldn't be if they apply to
multiple type of specs.
Each file should include only one context and have a descriptive name, e.g.
`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
### Helpers
Helpers are usually modules that provide some methods to hide the complexity of
specific RSpec examples. You can define helpers in RSpec files if they're not
intended to be shared with other specs. Otherwise, they should be be placed
under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
if they apply to multiple type of specs.
Helpers should follow the Rails naming / namespacing convention. For instance
`spec/support/helpers/cycle_analytics_helpers.rb` should define:
```ruby
module Spec
module Support
module Helpers
module CycleAnalyticsHelpers
def create_commit_referencing_issue(issue, branch_name: random_git_name)
project.repository.add_branch(user, branch_name, 'master')
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
end
end
end
end
```
Helpers should not change the RSpec config. For instance, the helpers module
described above should not include:
```ruby
RSpec.configure do |config|
config.include Spec::Support::Helpers::CycleAnalyticsHelpers
end
```
### Factories
GitLab uses [factory_girl] as a test fixture replacement.
- Factory definitions live in `spec/factories/`, named using the pluralization
of their corresponding model (`User` factories are defined in `users.rb`).
- There should be only one top-level factory definition per file.
- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
should) call `create(...)` instead of `FactoryGirl.create(...)`.
- Make use of [traits] to clean up definitions and usages.
- When defining a factory, don't define attributes that are not required for the
resulting record to pass validation.
- When instantiating from a factory, don't supply attributes that aren't
required by the test.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
[factory_girl]: https://github.com/thoughtbot/factory_girl
[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
### Fixtures
All fixtures should be be placed under `spec/fixtures/`.
### Config
RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under
`spec/support/config/`.
Each file should be related to a specific domain, e.g.
`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
Helpers can be included in the `spec/support/config/rspec.rb` file. If a
helpers module applies only to a certain kind of specs, it should add modifiers
to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following:
```ruby
RSpec.configure do |config|
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
end
```
## Testing Rake Tasks
To make testing Rake tasks a little easier, there is a helper that can be included
in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
a few other things to make testing Rake tasks easier.
At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available
methods.
Example:
```ruby
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/shell'
stub_warn_user_is_not_gitlab
end
describe 'install task' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
run_rake_task('gitlab:shell:install')
end
end
end
```
## Test speed
GitLab has a massive test suite that, without [parallelization], can take hours
to run. It's important that we make an effort to write tests that are accurate
and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
- `double` and `spy` are faster than `FactoryGirl.build(...)`
- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `@javascript` in
Spinach or `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
[parallelization]: #test-suite-parallelization-on-the-ci
### Test suite parallelization on the CI
Our current CI parallelization setup is as follows:
1. The `retrieve-tests-metadata` job in the `prepare` stage ensures that we have
a `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
- The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
from S3, if it's not here we initialize the file with `{}`.
1. Each `rspec-pg x y`/`rspec-mysql x y` job is run with `knapsack rspec` and
should have an evenly distributed share of tests:
- It works because the jobs have access to the
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
from all previous stages are passed by default". [^1]
- The jobs set their own report path to
`KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
- If knapsack is doing its job, test files that are run should be listed under
`Report specs`, not under `Leftover specs`.
1. The `update-tests-metadata` job takes all the
`knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
files from the `rspec-pg x y`/`rspec-mysql x y`jobs and merge them all together
into a single `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that
is then uploaded to S3.
After that, the next pipeline will use the up-to-date
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
is used for Spinach tests as well.
### Monitoring
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
A [public dashboard] is available for everyone to see. Feel free to look at the
slowest test files and try to improve them.
[monitored]: ./performance.md#rspec-profiling
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
- On CE and EE, the test suite runs both PostgreSQL and MySQL.
- Rails logging to `log/test.log` is disabled by default in CI [for
performance reasons][logging]. To override this setting, provide the
`RAILS_ENABLE_TEST_LOG` environment variable.
[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
for its feature/integration tests in September 2012.
As of March 2016, we are [trying to avoid adding new Spinach
tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
opting for [RSpec feature](#features-integration) specs.
Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
no more than one new `step` definition. If more than that is required, the
test should be re-implemented using RSpec instead.
---
[Return to Development documentation](README.md)
[^1]: /ci/yaml/README.html#dependencies
This document was moved to [testing_guide/index.md](testing_guide/index.md).
# Testing best practices
## Test speed
GitLab has a massive test suite that, without [parallelization], can take hours
to run. It's important that we make an effort to write tests that are accurate
and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
- `double` and `spy` are faster than `FactoryGirl.build(...)`
- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `@javascript` in
Spinach or `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
[parallelization]: ci.md#test-suite-parallelization-on-the-ci
## RSpec
### General guidelines
- Use a single, top-level `describe ClassName` block.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
### System / Feature tests
NOTE: **Note:** Before writing a new system test, [please consider **not**
writing one](testing_levels.md#consider-not-writing-a-system-test)!
- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
`user_changes_password_spec.rb`.
- Use scenario titles that describe the success and failure paths.
- Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title.
- Create only the necessary records in the database
- Test a happy path and a less happy path but that's it
- Every other possible path should be tested with Unit or Integration tests
- Test what's displayed on the page, not the internals of ActiveRecord models.
For instance, if you want to verify that a record was created, add
expectations that its attributes are displayed on the page, not that
`Model.count` increased by one.
- It's ok to look for DOM elements but don't abuse it since it makes the tests
more brittle
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
duplication. However, this sometimes [comes at the cost of clarity][lets-not],
so we need to set some guidelines for their use going forward:
- `let` variables are preferable to instance variables. Local variables are
preferable to `let` variables.
- Use `let` to reduce duplication throughout an entire spec file.
- Don't use `let` to define variables used by a single test; define them as
local variables inside the test's `it` block.
- Don't define a `let` variable inside the top-level `describe` block that's
only used in a more deeply-nested `context` or `describe` block. Keep the
definition as close as possible to where it's used.
- Try to avoid overriding the definition of one `let` variable with another.
- Don't define a `let` variable that's only used by the definition of another.
Use a helper method instead.
[lets-not]: https://robots.thoughtbot.com/lets-not
### `set` variables
In some cases there is no need to recreate the same object for tests again for
each example. For example, a project is needed to test issues on the same
project, one project will do for the entire file. This can be achieved by using
`set` in the same way you would use `let`.
`rspec-set` only works on ActiveRecord objects, and before new examples it
reloads or recreates the model, _only_ if needed. That is, when you changed
properties or destroyed the object.
There is one gotcha; you can't reference a model defined in a `let` block in a
`set` block.
### Time-sensitive tests
[Timecop](https://github.com/travisjeffery/timecop) is available in our
Ruby-based tests for verifying things that are time-sensitive. Any test that
exercises or verifies something time-sensitive should make use of Timecop to
prevent transient test failures.
Example:
```ruby
it 'is overdue' do
issue = build(:issue, due_date: Date.tomorrow)
Timecop.freeze(3.days.from_now) do
expect(issue).to be_overdue
end
end
```
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive
range of inputs. By specifying the test case once, alongside a table of inputs
and the expected output for each, your tests can be made easier to read and more
compact.
We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
gem. A short example, using the table syntax and checking Ruby equality for a
range of inputs, might look like this:
```ruby
describe "#==" do
using RSpec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
project1 | project1 | true
project2 | project2 | true
project 1 | project2 | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
```
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
complexity of RSpec expectations.They should be placed under
`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
they apply to multiple type of specs.
#### `have_gitlab_http_status`
Prefer `have_gitlab_http_status` over `have_http_status` because the former
could also show the response body whenever the status mismatched. This would
be very useful whenever some tests start breaking and we would love to know
why without editing the source and rerun the tests.
This is especially useful whenever it's showing 500 internal server error.
### Shared contexts
All shared contexts should be be placed under `spec/support/shared_contexts/`.
Shared contexts can be placed in subfolder if they apply to a certain type of
specs only (e.g. features, requests etc.) but shouldn't be if they apply to
multiple type of specs.
Each file should include only one context and have a descriptive name, e.g.
`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
### Shared examples
All shared examples should be be placed under `spec/support/shared_examples/`.
Shared examples can be placed in subfolder if they apply to a certain type of
specs only (e.g. features, requests etc.) but shouldn't be if they apply to
multiple type of specs.
Each file should include only one context and have a descriptive name, e.g.
`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
### Helpers
Helpers are usually modules that provide some methods to hide the complexity of
specific RSpec examples. You can define helpers in RSpec files if they're not
intended to be shared with other specs. Otherwise, they should be be placed
under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
if they apply to multiple type of specs.
Helpers should follow the Rails naming / namespacing convention. For instance
`spec/support/helpers/cycle_analytics_helpers.rb` should define:
```ruby
module Spec
module Support
module Helpers
module CycleAnalyticsHelpers
def create_commit_referencing_issue(issue, branch_name: random_git_name)
project.repository.add_branch(user, branch_name, 'master')
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
end
end
end
end
```
Helpers should not change the RSpec config. For instance, the helpers module
described above should not include:
```ruby
RSpec.configure do |config|
config.include Spec::Support::Helpers::CycleAnalyticsHelpers
end
```
### Factories
GitLab uses [factory_girl] as a test fixture replacement.
- Factory definitions live in `spec/factories/`, named using the pluralization
of their corresponding model (`User` factories are defined in `users.rb`).
- There should be only one top-level factory definition per file.
- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
should) call `create(...)` instead of `FactoryGirl.create(...)`.
- Make use of [traits] to clean up definitions and usages.
- When defining a factory, don't define attributes that are not required for the
resulting record to pass validation.
- When instantiating from a factory, don't supply attributes that aren't
required by the test.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
[factory_girl]: https://github.com/thoughtbot/factory_girl
[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
### Fixtures
All fixtures should be be placed under `spec/fixtures/`.
### Config
RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under
`spec/support/config/`.
Each file should be related to a specific domain, e.g.
`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
Helpers can be included in the `spec/support/config/rspec.rb` file. If a
helpers module applies only to a certain kind of specs, it should add modifiers
to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following:
```ruby
RSpec.configure do |config|
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
end
```
---
[Return to Testing documentation](index.md)
# GitLab tests in the Continuous Integration (CI) context
### Test suite parallelization on the CI
Our current CI parallelization setup is as follows:
1. The `knapsack` job in the prepare stage that is supposed to ensure we have a
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
- The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
from S3, if it's not here we initialize the file with `{}`.
1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly
distributed share of tests:
- It works because the jobs have access to the
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
from all previous stages are passed by default". [^1]
- the jobs set their own report path to
`KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
- if knapsack is doing its job, test files that are run should be listed under
`Report specs`, not under `Leftover specs`.
1. The `update-knapsack` job takes all the
`knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
files from the `rspec x y` jobs and merge them all together into a single
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then
uploaded to S3.
After that, the next pipeline will use the up-to-date
`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
is used for Spinach tests as well.
### Monitoring
The GitLab test suite is [monitored] for the `master` branch, and any branch
that includes `rspec-profile` in their name.
A [public dashboard] is available for everyone to see. Feel free to look at the
slowest test files and try to improve them.
[monitored]: ../performance.md#rspec-profiling
[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
## CI setup
- On CE and EE, the test suite runs both PostgreSQL and MySQL.
- Rails logging to `log/test.log` is disabled by default in CI [for
performance reasons][logging]. To override this setting, provide the
`RAILS_ENABLE_TEST_LOG` environment variable.
[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4
---
[Return to Testing documentation](index.md)
# Flaky tests
## What's a flaky test?
It's a test that sometimes fails, but if you retry it enough times, it passes,
eventually.
## Automatic retries and flaky tests detection
On our CI, we use [rspec-retry] to automatically retry a failing example a few
times (see [`spec/spec_helper.rb`] for the precise retries count).
We also use a home-made `RspecFlaky::Listener` listener which records flaky
examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
is detected in any other branch (`flaky-examples-check` job). In the future, the
`flaky-examples-check` job will not be allowed to fail.
This was originally implemented in: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13021.
[rspec-retry]: https://github.com/NoRedInk/rspec-retry
[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
## Problems we had in the past at GitLab
- [`rspec-retry` is bitting us when some API specs fail](https://gitlab.com/gitlab-org/gitlab-ce/issues/29242): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9825
- [Sporadic RSpec failures due to `PG::UniqueViolation`](https://gitlab.com/gitlab-org/gitlab-ce/issues/28307#note_24958837): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9846
- Follow-up: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10688
- [Capybara.reset_session! should be called before requests are blocked](https://gitlab.com/gitlab-org/gitlab-ce/issues/33779): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12224
- FFaker generates funky data that tests are not ready to handle (and tests should be predictable so that's bad!):
- [Make `spec/mailers/notify_spec.rb` more robust](https://gitlab.com/gitlab-org/gitlab-ce/issues/20121): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10015
- [Transient failure in spec/requests/api/commits_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/27988#note_25342521): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9944
- [Replace FFaker factory data with sequences](https://gitlab.com/gitlab-org/gitlab-ce/issues/29643): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10184
- [Transient failure in spec/finders/issues_finder_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/30211#note_26707685): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10404
### Time-sensitive flaky tests
- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10046
- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10306
### Array order expectation
- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10148
### Feature tests
- [Be sure to create all the data the test need before starting exercize](https://gitlab.com/gitlab-org/gitlab-ce/issues/32622#note_31128195): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12059
- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34609#note_34048715): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12604
- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34698#note_34276286): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12664
- [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-ce/issues/31437): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10934
#### Capybara viewport size related issues
- [Transient failure of spec/features/issues/filtered_search/filter_issues_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/29241#note_26743936): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10411
#### Capybara JS driver related issues
- [Don't wait for AJAX when no AJAX request is fired](https://gitlab.com/gitlab-org/gitlab-ce/issues/30461): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10454
- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34647): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12626
#### PhantomJS / WebKit related issues
- Memory is through the roof! (TL;DR: Load images but block images requests!): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12003
## Resources
- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html)
- [How to Deal With and Eliminate Flaky Tests](https://semaphoreci.com/community/tutorials/how-to-deal-with-and-eliminate-flaky-tests)
- [Tips on Treating Flakiness in your Rails Test Suite](http://semaphoreci.com/blog/2017/08/03/tips-on-treating-flakiness-in-your-test-suite.html)
- ['Flaky' tests: a short story](https://www.ombulabs.com/blog/rspec/continuous-integration/how-to-track-down-a-flaky-test.html)
- [Using Insights to Discover Flaky, Slow, and Failed Tests](https://circleci.com/blog/using-insights-to-discover-flaky-slow-and-failed-tests/)
---
[Return to Testing documentation](index.md)
# Frontend testing standards and style guidelines
There are two types of test suites you'll encounter while developing frontend code
at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing,
and RSpec feature tests with Capybara for e2e (end-to-end) integration testing.
Unit and feature tests need to be written for all new features.
Most of the time, you should use [RSpec] for your feature tests.
Regression tests should be written for bug fixes to prevent them from recurring
in the future.
See the [Testing Standards and Style Guidelines](index.md) page for more
information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine] as its test
framework for our JavaScript unit and integration tests. For integration tests,
we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples).
Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`).
Adding these static fixtures should be avoided as they are harder to keep up to date with real views.
The existing static fixtures will be migrated over time.
Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress.
Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
JavaScript tests live in `spec/javascripts/`, matching the folder structure
of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
Keep in mind that in a CI environment, these tests are run in a headless
browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
### Best practices
#### Naming unit tests
When writing describe test blocks to test specific functions/methods,
please use the method name as the describe block name.
```javascript
// Good
describe('methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
// Bad
describe('#methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
// Bad
describe('.methodName', () => {
it('passes', () => {
expect(true).toEqual(true);
});
});
```
#### Testing promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred.
```javascript
// Good
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(done.fail);
});
// Good
it('tests a promise rejection', (done) => {
promise
.then(done.fail)
.catch((error) => {
expect(error).toBe(expectedError);
})
.then(done)
.catch(done.fail);
});
// Bad (missing done callback)
it('tests a promise', () => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
});
// Bad (missing catch)
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
});
// Bad (use done.fail in asynchronous tests)
it('tests a promise', (done) => {
promise
.then((data) => {
expect(data).toBe(asExpected);
})
.then(done)
.catch(fail)
});
// Bad (missing catch)
it('tests a promise rejection', (done) => {
promise
.catch((error) => {
expect(error).toBe(expectedError);
})
.then(done)
});
```
#### Stubbing
For unit tests, you should stub methods that are unrelated to the current unit you are testing.
If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
For integration tests, you should stub methods that will effect the stability of the test if they
execute their original behaviour. i.e. Network requests.
### Vue.js unit tests
See this [section][vue-test].
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
### Live testing and focused testing
While developing locally, it may be helpful to keep karma running so that you
can get instant feedback on as you write tests and modify code. To do this
you can start karma with `npm run karma-start`. It will compile the javascript
assets and run a server at `http://localhost:9876/` where it will automatically
run the tests on any browser which connects to it. You can enter that url on
multiple browsers at once to have it run the tests on each in parallel.
While karma is running, any changes you make will instantly trigger a recompile
and retest of the entire test suite, so you can see instantly if you've broken
a test with your changes. You can use [jasmine focused][jasmine-focus] or
excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
## RSpec feature integration tests
Information on setting up and running RSpec integration tests with
[Capybara] can be found in the [Testing Best Practices](best_practices.md).
## Gotchas
### Errors due to use of unsupported JavaScript features
Similar errors will be thrown if you're using JavaScript features not yet
supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
- Array.first
- Async functions
- Generators
- Array destructuring
- For..Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
### RSpec errors due to JavaScript
By default RSpec unit tests will not run JavaScript in the headless browser
and will simply rely on inspecting the HTML generated by rails.
If an integration test depends on JavaScript to run correctly, you need to make
sure the spec is configured to enable JavaScript when the tests are run. If you
don't do this you'll see vague error messages from the spec runner.
To enable a JavaScript driver in an `rspec` test, add `:js` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', :js do
# assertions...
end
describe "Admin::AbuseReports", :js do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
### Spinach errors due to missing JavaScript
NOTE: **Note:** Since we are discouraging the use of Spinach when writing new
feature tests, you shouldn't ever need to use this. This information is kept
available for legacy purposes only.
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.github.io/
---
[Return to Testing documentation](index.md)
# Testing standards and style guidelines
This document describes various guidelines and best practices for automated
testing of the GitLab project.
It is meant to be an _extension_ of the [thoughtbot testing
styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
this guide defines a rule that contradicts the thoughtbot guide, this guide
takes precedence. Some guidelines may be repeated verbatim to stress their
importance.
## Overview
GitLab is built on top of [Ruby on Rails][rails], and we're using [RSpec] for all
the backend tests, with [Capybara] for end-to-end integration testing.
On the frontend side, we're using [Karma] and [Jasmine] for JavaScript unit and
integration testing.
Following are two great articles that everyone should read to understand what
automated testing means, and what are its principles:
- [Five Factor Testing](https://www.devmynd.com/blog/five-factor-testing): Why do we need tests?
- [Principles of Automated Testing](http://www.lihaoyi.com/post/PrinciplesofAutomatedTesting.html): Levels of testing. Prioritize tests. Cost of tests.
---
## [Testing levels](testing_levels.md)
Learn about the different testing levels, and how to decide at what level your
changes should be tested.
---
## [Testing best practices](best_practices.md)
Everything you should know about how to write good tests: RSpec, FactoryGirl,
system tests, parameterized tests etc.
---
## [Frontend testing standards and style guidelines](frontend_testing.md)
Everything you should know about how to write good Frontend tests: Karma,
testing promises, stubbing etc.
---
## [Flaky tests](flaky_tests.md)
What are flaky tests, the different kind of flaky tests we encountered, and what
we do about them.
---
## [GitLab tests in the Continuous Integration (CI) context](ci.md)
How GitLab test suite is run in the CI context: setup, caches, artifacts,
parallelization, monitoring.
---
## [Testing Rake tasks](testing_rake_tasks.md)
Everything you should know about how to test Rake tasks.
---
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
for its feature/integration tests in September 2012.
As of March 2016, we are [trying to avoid adding new Spinach
tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
opting for [RSpec feature](#features-integration) specs.
Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
no more than one new `step` definition. If more than that is required, the
test should be re-implemented using RSpec instead.
---
[Return to Development documentation](../README.md)
[^1]: /ci/yaml/README.html#dependencies
[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
[Capybara]: https://github.com/teamcapybara/capybara
[Karma]: http://karma-runner.github.io/
[Jasmine]: https://jasmine.github.io/
# Testing levels
![Testing priority triangle](img/testing_triangle.png)
_This diagram demonstrates the relative priority of each test type we use. `e2e` stands for end-to-end._
## Unit tests
Formal definition: https://en.wikipedia.org/wiki/Unit_testing
These kind of tests ensure that a single unit of code (a method) works as
expected (given an input, it has a predictable output). These tests should be
isolated as much as possible. For example, model methods that don't do anything
with the database shouldn't need a DB record. Classes that don't need database
records should use stubs/doubles as much as possible.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
| `app/finders/` | `spec/finders/` | RSpec | |
| `app/helpers/` | `spec/helpers/` | RSpec | |
| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
| `app/policies/` | `spec/policies/` | RSpec | |
| `app/presenters/` | `spec/presenters/` | RSpec | |
| `app/routing/` | `spec/routing/` | RSpec | |
| `app/serializers/` | `spec/serializers/` | RSpec | |
| `app/services/` | `spec/services/` | RSpec | |
| `app/tasks/` | `spec/tasks/` | RSpec | |
| `app/uploaders/` | `spec/uploaders/` | RSpec | |
| `app/views/` | `spec/views/` | RSpec | |
| `app/workers/` | `spec/workers/` | RSpec | |
| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontent Testing guide](frontend_testing.md) section. |
## Integration tests
Formal definition: https://en.wikipedia.org/wiki/Integration_testing
These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
| `app/controllers/` | `spec/controllers/` | RSpec | |
| `app/mailers/` | `spec/mailers/` | RSpec | |
| `lib/api/` | `spec/requests/api/` | RSpec | |
| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
### About controller tests
In an ideal world, controllers should be thin. However, when this is not the
case, it's acceptable to write a system/feature test without JavaScript instead
of a controller test. The reason is that testing a fat controller usually
involves a lot of stubbing, things like:
```ruby
controller.instance_variable_set(:@user, user)
```
and use methods which are deprecated in Rails 5 ([#23768]).
[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
### About Karma
As you may have noticed, Karma is both in the Unit tests and the Integration
tests category. That's because Karma is a tool that provides an environment to
run JavaScript tests, so you can either run unit tests (e.g. test a single
JavaScript method), or integration tests (e.g. test a component that is composed
of multiple components).
## System tests or Feature tests
Formal definition: https://en.wikipedia.org/wiki/System_testing.
These kind of tests ensure the application works as expected from a user point
of view (aka black-box testing). These tests should test a happy path for a
given page or set of pages, and a test case should be added for any regression
that couldn't have been caught at lower levels with better tests (i.e. if a
regression is found, regression tests should be added at the lowest-level
possible).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
### Consider **not** writing a system test!
If we're confident that the low-level components work well (and we should be if
we have enough Unit & Integration tests), we shouldn't need to duplicate their
thorough testing at the System test level.
It's very easy to add tests, but a lot harder to remove or improve tests, so one
should take care of not introducing too many (slow and duplicated) specs.
The reasons why we should follow these best practices are as follows:
- System tests are slow to run since they spin up the entire application stack
in a headless browser, and even slower when they integrate a JS driver
- When system tests run with a JavaScript driver, the tests are run in a
different thread than the application. This means it does not share a
database connection and your test will have to commit the transactions in
order for the running application to see the data (and vice-versa). In that
case we need to truncate the database after each spec instead of simply
rolling back a transaction (the faster strategy that's in use for other kind
of tests). This is slower than transactions, however, so we want to use
truncation only when necessary.
[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
[RackTest]: https://github.com/teamcapybara/capybara#racktest
## Black-box tests or End-to-end tests
GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
are configured and packaged by [GitLab Omnibus].
[GitLab QA] is a tool that allows to test that all these pieces integrate well
together by building a Docker image for a given version of GitLab Rails and
running feature tests (i.e. using Capybara) against it.
The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase.
[multiple pieces]: ../architecture.md#components
[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[Gitaly]: https://gitlab.com/gitlab-org/gitaly
[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
## EE-specific tests
EE-specific tests follows the same organization, but under the `spec/ee` folder.
## How to test at the correct level?
As many things in life, deciding what to test at each level of testing is a
trade-off:
- Unit tests are usually cheap, and you should consider them like the basement
of your house: you need them to be confident that your code is behaving
correctly. However if you run only unit tests without integration / system
tests, you might [miss] the [big] [picture]!
- Integration tests are a bit more expensive, but don't abuse them. A system test
is often better than an integration test that is stubbing a lot of internals.
- System tests are expensive (compared to unit tests), even more if they require
a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
section.
Another way to see it is to think about the "cost of tests", this is well
explained [in this article][tests-cost] and the basic idea is that the cost of a
test includes:
- The time it takes to write the test
- The time it takes to run the test every time the suite runs
- The time it takes to understand the test
- The time it takes to fix the test if it breaks and the underlying code is OK
- Maybe, the time it takes to change the code to make the code testable.
### Frontend-related tests
There are cases where the behaviour you are testing is not worth the time spent
running the full application, for example, if you are testing styling, animation,
edge cases or small actions that don't involve the backend,
you should write an integration test using Jasmine.
[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
[big]: https://twitter.com/timbray/status/822470746773409794
[picture]: https://twitter.com/withzombies/status/829716565834752000
[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
---
[Return to Testing documentation](index.md)
## Testing Rake tasks
To make testing Rake tasks a little easier, there is a helper that can be included
in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
a few other things to make testing Rake tasks easier.
At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available
methods.
Example:
```ruby
require 'rake_helper'
describe 'gitlab:shell rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/shell'
stub_warn_user_is_not_gitlab
end
describe 'install task' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
run_rake_task('gitlab:shell:install')
end
end
end
```
---
[Return to Testing documentation](index.md)
......@@ -287,6 +287,22 @@ Finally, it's possible that the browser or client machine lack Kerberos support
completely. Ensure that the Kerberos libraries are installed and that you can
authenticate to other Kerberos services.
### HTTP Basic: Access denied when cloning
```sh
remote: HTTP Basic: Access denied
fatal: Authentication failed for '<KRB5 path>'
```
If you are using Git v2.11 or newer and see the above error when cloning, you can
set the `http.emptyAuth` Git option to `true` to fix this:
```
git config --global http.emptyAuth true
```
See also: [Git v2.11 release notes](https://github.com/git/git/blob/master/Documentation/RelNotes/2.11.0.txt#L482-L486)
## Helpful links
- <https://help.ubuntu.com/community/Kerberos>
......
......@@ -8,6 +8,10 @@ The license has the form of a base64 encoded ASCII text with a `.gitlab-license`
extension and can be obtained when you [purchase one][pricing] or when you sign
up for a [free trial].
> **Note:** As of GitLab Enterprise Edition 9.4.0, a new EE instance without a valid license
will only have CE features active. A trial license will activate all EE features,
but the instance will fall back to CE only functionality after the trial expires.
## Uploading your license
The very first time you visit your GitLab EE installation, you should see a
......
/* eslint-disable no-underscore-dangle, class-methods-use-this */
/* global Flash */
import Flash from '~/flash';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedBranchAccessDropdown {
......
/* global Flash */
import AccessorUtilities from '~/lib/utils/accessor';
import Flash from '~/flash';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown';
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '~/flash';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
......
/* eslint-disable no-underscore-dangle, class-methods-use-this */
/* global Flash */
import Flash from '~/flash';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedTagAccessDropdown {
......
/* global Flash */
import Flash from '~/flash';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
......
/* eslint-disable no-new */
/* global Flash */
import Flash from '~/flash';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
......
/* global Flash */
import Flash from '~/flash';
import MRWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author';
import eventHub from '~/vue_merge_request_widget/event_hub';
......
/* global Flash */
import Flash from '~/flash';
import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar';
import eventHub from '~/vue_merge_request_widget/event_hub';
......
/* global Flash */
import Flash from '~/flash';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon';
import ApprovalsBody from './approvals_body';
import ApprovalsFooter from './approvals_footer';
......@@ -83,4 +82,3 @@ export default {
</div>
`,
};
<script>
/* global Flash */
import simplePoll from '~/lib/utils/simple_poll';
import eventHub from '~/vue_merge_request_widget/event_hub';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import '~/flash';
import Flash from '~/flash';
export default {
props: {
......
......@@ -18,7 +18,7 @@ module EE
def build_issue_link_base
return super unless @board.group_board?
"/#{@board.group.path}/:project_path/issues"
"#{group_path(@board.group)}/:project_path/issues"
end
def board_base_url
......
- if Gitlab::LDAP::Config.enabled_extras? && can?(current_user, :admin_ldap_group_links, @group)
- if Gitlab::LDAP::Config.group_sync_enabled? && can?(current_user, :admin_ldap_group_links, @group)
= nav_link(path: 'ldap_group_links#index') do
= link_to group_ldap_group_links_path(@group), title: 'LDAP Group' do
%span
LDAP Group
LDAP Synchronization
- if @group.feature_available?(:group_webhooks) || show_promotions?
= nav_link(path: 'hooks#index') do
......
......@@ -5,8 +5,8 @@ module EE
extend ActiveSupport::Concern
class_methods do
def enabled_extras?
enabled? && ::License.feature_available?(:ldap_extras)
def group_sync_enabled?
enabled? && ::License.feature_available?(:ldap_group_sync)
end
end
end
......
......@@ -74,6 +74,8 @@ module EE
access_levels = AccessLevels.new
# Only iterate over group links for the current provider
group.ldap_group_links.with_provider(provider).each do |group_link|
next unless group_link.active?
update_access_levels(access_levels, group_link)
end
......
......@@ -213,7 +213,7 @@ module API
desc 'Sync a group with LDAP.'
post ":id/ldap_sync" do
not_found! unless Gitlab::LDAP::Config.enabled_extras?
not_found! unless Gitlab::LDAP::Config.group_sync_enabled?
group = find_group!(params[:id])
authorize! :admin_group, group
......
......@@ -27,7 +27,7 @@ module Gitlab
def self.available_servers
return [] unless enabled?
enabled_extras? ? servers : Array.wrap(servers.first)
::License.feature_available?(:multiple_ldap_servers) ? servers : Array.wrap(servers.first)
end
def self.providers
......
......@@ -381,6 +381,10 @@ describe EE::Gitlab::LDAP::Sync::Group do
end
context 'filter' do
before do
stub_licensed_features(ldap_group_sync_filter: true)
end
describe '#update_permissions' do
before do
# Safe-check because some permissions are removed when `Group#ldap_synced?`
......
......@@ -19,7 +19,7 @@ feature 'Edit group settings' do
scenario 'is able to navigate to LDAP group section' do
visit edit_group_path(group)
expect(find('.nav-sidebar')).to have_content('LDAP Group')
expect(find('.nav-sidebar')).to have_content('LDAP Synchronization')
end
context 'with owners not being able to manage LDAP' do
......@@ -28,7 +28,7 @@ feature 'Edit group settings' do
visit edit_group_path(group)
expect(find('.nav-sidebar')).not_to have_content('LDAP Group')
expect(find('.nav-sidebar')).not_to have_content('LDAP Synchronization')
end
end
end
......
......@@ -12,23 +12,49 @@ feature 'Edit group settings', :js do
context 'LDAP sync method' do
before do
allow(Gitlab.config.ldap).to receive(:enabled).and_return(true)
visit group_ldap_group_links_path(group)
end
scenario 'shows the LDAP filter section' do
choose('sync_method_filter')
context 'when the LDAP group sync filter feature is available' do
before do
stub_licensed_features(ldap_group_sync_filter: true)
visit group_ldap_group_links_path(group)
end
scenario 'shows the LDAP filter section' do
choose('sync_method_filter')
expect(page).to have_content('This query must use valid LDAP Search Filter Syntax')
expect(page).not_to have_content("Synchronize #{group.name}'s members with this LDAP group")
end
expect(page).to have_content('This query must use valid LDAP Search Filter Syntax')
expect(page).not_to have_content("Synchronize #{group.name}'s members with this LDAP group")
scenario 'shows the LDAP group section' do
choose('sync_method_filter') # choose filter first, as group's the default
choose('sync_method_group')
expect(page).to have_content("Synchronize #{group.name}'s members with this LDAP group")
expect(page).not_to have_content('This query must use valid LDAP Search Filter Syntax')
end
end
scenario 'shows the LDAP group section' do
choose('sync_method_filter') # choose filter first, as group's the default
choose('sync_method_group')
context 'when the LDAP group sync filter feature is available' do
before do
stub_licensed_features(ldap_group_sync_filter: false)
visit group_ldap_group_links_path(group)
end
scenario 'does not show the LDAP search method switcher' do
expect(page).not_to have_field('sync_method_filter')
end
scenario 'shows the LDAP group section' do
expect(page).to have_content("Synchronize #{group.name}'s members with this LDAP group")
end
expect(page).to have_content("Synchronize #{group.name}'s members with this LDAP group")
expect(page).not_to have_content('This query must use valid LDAP Search Filter Syntax')
scenario 'does not shows the LDAP filter section' do
expect(page).not_to have_content('This query must use valid LDAP Search Filter Syntax')
end
end
end
end
require 'spec_helper'
describe BoardsHelper do
describe '#build_issue_link_base' do
it 'returns correct path for project board' do
@project = create(:project)
@board = create(:board, project: @project)
expect(build_issue_link_base).to eq("/#{@project.namespace.path}/#{@project.path}/issues")
end
context 'group board' do
let(:base_group) { create(:group, path: 'base') }
it 'returns correct path for base group' do
@board = create(:board, group: base_group)
expect(build_issue_link_base).to eq('/base/:project_path/issues')
end
it 'returns correct path for subgroup' do
subgroup = create(:group, parent: base_group, path: 'sub')
@board = create(:board, group: subgroup)
expect(build_issue_link_base).to eq('/base/sub/:project_path/issues')
end
end
end
end
import flash, {
createFlashEl,
createAction,
hideFlash,
} from '~/flash';
describe('Flash', () => {
describe('createFlashEl', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
afterEach(() => {
el.innerHTML = '';
});
it('creates flash element with type', () => {
el.innerHTML = createFlashEl('testing', 'alert');
expect(
el.querySelector('.flash-alert'),
).not.toBeNull();
});
it('escapes text', () => {
el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert');
expect(
el.querySelector('.flash-text').textContent.trim(),
).toBe('<script>alert("a");</script>');
});
it('adds container classes when inside content wrapper', () => {
el.innerHTML = createFlashEl('testing', 'alert', true);
expect(
el.querySelector('.flash-text').classList.contains('container-fluid'),
).toBeTruthy();
expect(
el.querySelector('.flash-text').classList.contains('container-limited'),
).toBeTruthy();
});
});
describe('hideFlash', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
el.className = 'js-testing';
});
it('sets transition style', () => {
hideFlash(el);
expect(
el.style.transition,
).toBe('opacity 0.3s');
});
it('sets opacity style', () => {
hideFlash(el);
expect(
el.style.opacity,
).toBe('0');
});
it('does not set styles when fadeTransition is false', () => {
hideFlash(el, false);
expect(
el.style.opacity,
).toBe('');
expect(
el.style.transition,
).toBe('');
});
it('removes element after transitionend', () => {
document.body.appendChild(el);
hideFlash(el);
el.dispatchEvent(new Event('transitionend'));
expect(
document.querySelector('.js-testing'),
).toBeNull();
});
it('calls event listener callback once', () => {
spyOn(el, 'remove').and.callThrough();
document.body.appendChild(el);
hideFlash(el);
el.dispatchEvent(new Event('transitionend'));
el.dispatchEvent(new Event('transitionend'));
expect(
el.remove.calls.count(),
).toBe(1);
});
});
describe('createAction', () => {
let el;
beforeEach(() => {
el = document.createElement('div');
});
it('creates link with href', () => {
el.innerHTML = createAction({
href: 'testing',
title: 'test',
});
expect(
el.querySelector('.flash-action').href,
).toContain('testing');
});
it('uses hash as href when no href is present', () => {
el.innerHTML = createAction({
title: 'test',
});
expect(
el.querySelector('.flash-action').href,
).toContain('#');
});
it('adds role when no href is present', () => {
el.innerHTML = createAction({
title: 'test',
});
expect(
el.querySelector('.flash-action').getAttribute('role'),
).toBe('button');
});
it('escapes the title text', () => {
el.innerHTML = createAction({
title: '<script>alert("a")</script>',
});
expect(
el.querySelector('.flash-action').textContent.trim(),
).toBe('<script>alert("a")</script>');
});
});
describe('createFlash', () => {
describe('no flash-container', () => {
it('does not add to the DOM', () => {
const flashEl = flash('testing');
expect(
flashEl,
).toBeNull();
expect(
document.querySelector('.flash-alert'),
).toBeNull();
});
});
describe('with flash-container', () => {
beforeEach(() => {
document.body.innerHTML += `
<div class="content-wrapper js-content-wrapper">
<div class="flash-container"></div>
</div>
`;
});
afterEach(() => {
document.querySelector('.js-content-wrapper').remove();
});
it('adds flash element into container', () => {
flash('test');
expect(
document.querySelector('.flash-alert'),
).not.toBeNull();
});
it('adds flash into specified parent', () => {
flash(
'test',
'alert',
document.querySelector('.content-wrapper'),
);
expect(
document.querySelector('.content-wrapper .flash-alert'),
).not.toBeNull();
});
it('adds container classes when inside content-wrapper', () => {
flash('test');
expect(
document.querySelector('.flash-text').className,
).toBe('flash-text container-fluid container-limited');
});
it('does not add container when outside of content-wrapper', () => {
document.querySelector('.content-wrapper').className = 'js-content-wrapper';
flash('test');
expect(
document.querySelector('.flash-text').className.trim(),
).toBe('flash-text');
});
it('removes element after clicking', () => {
flash('test', 'alert', document, null, false);
document.querySelector('.flash-alert').click();
expect(
document.querySelector('.flash-alert'),
).toBeNull();
});
describe('with actionConfig', () => {
it('adds action link', () => {
flash(
'test',
'alert',
document,
{
title: 'test',
},
);
expect(
document.querySelector('.flash-action'),
).not.toBeNull();
});
it('calls actionConfig clickHandler on click', () => {
const actionConfig = {
title: 'test',
clickHandler: jasmine.createSpy('actionConfig'),
};
flash(
'test',
'alert',
document,
actionConfig,
);
document.querySelector('.flash-action').click();
expect(
actionConfig.clickHandler,
).toHaveBeenCalled();
});
});
});
});
});
......@@ -138,9 +138,9 @@ describe('IntegrationSettingsForm', () => {
deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' });
const $flashContainer = $('.flash-container');
expect($flashContainer.find('.flash-text').text()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-action')).toBeDefined();
expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
});
it('should submit form if ajax request responds without any error in test', () => {
......@@ -168,7 +168,7 @@ describe('IntegrationSettingsForm', () => {
expect($flashAction).toBeDefined();
spyOn(integrationSettingsForm.$form, 'submit');
$flashAction.trigger('click');
$flashAction.get(0).click();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
});
......@@ -181,7 +181,7 @@ describe('IntegrationSettingsForm', () => {
deferred.reject();
expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
});
it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
......
......@@ -815,7 +815,7 @@ import '~/notes';
});
it('shows a flash message', () => {
this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline);
this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
......@@ -828,7 +828,7 @@ import '~/notes';
});
it('hides visible flash message', () => {
this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline);
this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
this.notes.clearFlash();
......
......@@ -700,9 +700,13 @@ describe API::Groups do
end
describe 'POST /groups/:id/ldap_sync' do
context 'when LDAP config enabled_extras is true' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
end
context 'when the ldap_group_sync feature is available' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled_extras?).and_return(true)
stub_licensed_features(ldap_group_sync: true)
end
context 'when authenticated as the group owner' do
......@@ -772,9 +776,9 @@ describe API::Groups do
end
end
context 'when LDAP config enabled_extras is false' do
context 'when the ldap_group_sync feature is not available' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled_extras?).and_return(false)
stub_licensed_features(ldap_group_sync: false)
end
it 'returns 404 (same as CE would)' do
......
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