diff options
author | Phil Hughes <me@iamphill.com> | 2017-09-21 09:01:51 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-09-21 09:01:51 +0000 |
commit | 598b1a17a0442f0038e208f1abfc1198112282f3 (patch) | |
tree | 379289b3e94e1ded82a92291a1bd9ebecc79f4ef | |
parent | ac044ba0d53f17833f0aa97db404e9b49c9b219a (diff) | |
parent | 8784d362abd46c91791e783629621e4d3eaa7f08 (diff) | |
download | gitlab-ce-598b1a17a0442f0038e208f1abfc1198112282f3.tar.gz |
Merge branch '37220-es-modules' into 'master'
Use modules in common utils
See merge request gitlab-org/gitlab-ce!14074
41 files changed, 931 insertions, 870 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 22fa1f2a609..ec5be8664b2 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -2,6 +2,7 @@ /* global Flash */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -237,7 +238,7 @@ class AwardsHandler { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length; - if (gl.utils.isInIssuePage() && !isMainAwardsBlock) { + if (isInIssuePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); $('.emoji-menu').removeClass('is-visible'); @@ -288,7 +289,7 @@ class AwardsHandler { } getVotesBlock() { - if (gl.utils.isInIssuePage()) { + if (isInIssuePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { @@ -452,11 +453,11 @@ class AwardsHandler { userAuthored($emojiButton) { const oldTitle = this.getAwardTooltip($emojiButton); const newTitle = 'You cannot vote on your own issue, MR and note'; - gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); + updateTooltipTitle($emojiButton, newTitle).tooltip('show'); // Restore tooltip back to award list return setTimeout(() => { $emojiButton.tooltip('hide'); - gl.utils.updateTooltipTitle($emojiButton, oldTitle); + updateTooltipTitle($emojiButton, oldTitle); }, 2800); } diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 79702c54852..2cf8f4fa935 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,5 @@ import '../commons/bootstrap'; +import { isInIssuePage } from '../lib/utils/common_utils'; // Quick Submit behavior // @@ -45,7 +46,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); - if (!gl.utils.isInIssuePage()) { + if (!isInIssuePage()) { $submitButton.disable(); } } diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 187fab084fd..e0b73f13d36 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,4 +1,6 @@ /* global Flash */ +import { handleLocationHash } from '../../lib/utils/common_utils'; + export default class BlobViewer { constructor() { BlobViewer.initAuxiliaryViewer(); @@ -114,7 +116,7 @@ export default class BlobViewer { $(viewer).renderGFM(); this.$fileHolder.trigger('highlight:line'); - gl.utils.handleLocationHash(); + handleLocationHash(); this.toggleCopyButtonState(); }) diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 43928e602d6..ea82958e80d 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -2,6 +2,7 @@ /* global List */ import _ from 'underscore'; import Cookies from 'js-cookie'; +import { getUrlParamsArray } from '../../lib/utils/common_utils'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -21,7 +22,7 @@ gl.issueBoards.BoardsStore = { }, create () { this.state.lists = []; - this.filter.path = gl.utils.getUrlParamsArray().join('&'); + this.filter.path = getUrlParamsArray().join('&'); this.detail = { issue: {} }; }, addList (listObj, defaultAvatar) { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index ae1a23132a7..286a758b8a9 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -3,6 +3,7 @@ consistent-return, prefer-rest-params */ import _ from 'underscore'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; window.Build = (function () { Build.timeout = null; @@ -169,7 +170,7 @@ window.Build = (function () { data: this.state, }) .done((log) => { - gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + setCiStatusFavicon(`${this.pageUrl}/status.json`); if (log.state) { this.state = log.state; diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index b375b61202e..eae4a7eab55 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ +import { rstrip } from './lib/utils/common_utils'; window.ConfirmDangerModal = (function() { function ConfirmDangerModal(form, text) { @@ -12,7 +13,7 @@ window.ConfirmDangerModal = (function() { submit.disable(); $('.js-confirm-danger-input').off('input'); $('.js-confirm-danger-input').on('input', function() { - if (gl.utils.rstrip($(this).val()) === project_path) { + if (rstrip($(this).val()) === project_path) { return submit.enable(); } else { return submit.disable(); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 13ba4a57293..e3e2c798570 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import _ from 'underscore'; -import './lib/utils/common_utils'; +import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils'; import { placeholderImage } from './lazy_loader'; const gfmRules = { @@ -295,7 +295,7 @@ class CopyAsGFM { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; - const documentFragment = window.gl.utils.getSelectedFragment(); + const documentFragment = getSelectedFragment(); if (!documentFragment) return; const el = transformer(documentFragment.cloneNode(true)); @@ -412,7 +412,7 @@ class CopyAsGFM { for (const selector in rules) { const func = rules[selector]; - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + if (!nodeMatchesSelector(node, selector)) continue; let result; if (func.length === 2) { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f3b537c83e2..31214818496 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -77,6 +77,7 @@ import initProjectVisibilitySelector from './project_visibility'; import GpgBadges from './gpg_badges'; import UserFeatureHelper from './helpers/user_feature_helper'; import initChangesDropdown from './init_changes_dropdown'; +import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; (function() { var Dispatcher; @@ -100,7 +101,7 @@ import initChangesDropdown from './init_changes_dropdown'; $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); gfm.setup($(el), { emojis: true, members: enableGFM, @@ -351,7 +352,7 @@ import initChangesDropdown from './init_changes_dropdown'; if ($('.blob-viewer').length) new BlobViewer(); if ($('.project-show-activity').length) new gl.Activities(); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:edit': @@ -427,7 +428,7 @@ import initChangesDropdown from './init_changes_dropdown'; new NewCommitForm($('.js-create-dir-form')); new UserCallout({ setCalloutPerProject: true }); $('#tree-slider').waitForImages(function() { - gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); break; case 'projects:find_file:show': diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index f54d573db6e..14fde1afb16 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -6,7 +6,7 @@ import environmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; import eventHub from '../event_hub'; import Poll from '../../lib/utils/poll'; import environmentsMixin from '../mixins/environments_mixin'; @@ -51,19 +51,19 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, canCreateEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); + return convertPermissionToBoolean(this.canCreateEnvironment); }, }, @@ -72,8 +72,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -126,15 +126,15 @@ export default { * @return {String} */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 925503a01c4..35891240239 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -9,7 +9,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import Poll from '../../lib/utils/poll'; import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; -import '../../lib/utils/common_utils'; +import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { components: { @@ -47,15 +47,15 @@ export default { computed: { scope() { - return gl.utils.getParameterByName('scope'); + return getParameterByName('scope'); }, canReadEnvironmentParsed() { - return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); + return convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); + return convertPermissionToBoolean(this.canCreateDeployment); }, /** @@ -82,8 +82,8 @@ export default { * Toggles loading property. */ created() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.service = new EnvironmentsService(this.endpoint); @@ -125,15 +125,15 @@ export default { * @param {Number} pageNumber desired page to go to. */ changePage(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; }, fetchEnvironments() { - const scope = gl.utils.getParameterByName('scope') || this.visibility; - const page = gl.utils.getParameterByName('page') || this.pageNumber; + const scope = getParameterByName('scope') || this.visibility; + const page = getParameterByName('page') || this.pageNumber; this.isLoading = true; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 038c149be2d..aff8227c38c 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; /** * Environments Store. * @@ -66,8 +66,8 @@ export default class EnvironmentsStore { } setPagination(pagination = {}) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + const paginationInformation = parseIntPagination(normalizedHeaders); this.state.paginationInformation = paginationInformation; return paginationInformation; diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 36a04d4202f..d17a43b048a 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,6 +1,7 @@ <script> import tablePagination from '~/vue_shared/components/table_pagination.vue'; import eventHub from '../event_hub'; +import { getParameterByName } from '../../lib/utils/common_utils'; export default { props: { @@ -18,8 +19,8 @@ export default { }, methods: { change(page) { - const filterGroupsParam = gl.utils.getParameterByName('filter_groups'); - const sortParam = gl.utils.getParameterByName('sort'); + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam); }, }, diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 439a931ddad..83b102764ba 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,5 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath }) { @@ -54,7 +55,7 @@ export default class GroupFilterableList extends FilterableList { e.preventDefault(); const queryData = {}; - const sortParam = gl.utils.getParameterByName('sort', e.currentTarget.href); + const sortParam = getParameterByName('sort', e.currentTarget.href); if (sortParam) { queryData.sort = sortParam; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 00e1bd94c9c..9ad8e5c6052 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -8,6 +8,7 @@ import GroupItem from './components/group_item.vue'; import GroupsStore from './stores/groups_store'; import GroupsService from './services/groups_service'; import eventHub from './event_hub'; +import { getParameterByName } from '../lib/utils/common_utils'; document.addEventListener('DOMContentLoaded', () => { const el = document.getElementById('dashboard-group-app'); @@ -58,17 +59,17 @@ document.addEventListener('DOMContentLoaded', () => { this.isLoading = true; } - pageParam = gl.utils.getParameterByName('page'); + pageParam = getParameterByName('page'); if (pageParam) { page = pageParam; } - filterGroupsParam = gl.utils.getParameterByName('filter_groups'); + filterGroupsParam = getParameterByName('filter_groups'); if (filterGroupsParam) { filterGroups = filterGroupsParam; } - sortParam = gl.utils.getParameterByName('sort'); + sortParam = getParameterByName('sort'); if (sortParam) { sort = sortParam; } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 6eab6083e8f..f59ec677603 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; export default class GroupsStore { constructor() { @@ -30,8 +31,8 @@ export default class GroupsStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 4d629bc6326..90ca70289ab 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -4,6 +4,7 @@ prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, promise/catch-or-return */ import Api from './api'; +import { normalizeCRLFHeaders } from './lib/utils/common_utils'; var slice = [].slice; @@ -30,7 +31,7 @@ window.GroupsSelect = (function() { $.ajax(params).then((data, status, xhr) => { const results = data || []; - const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders()); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; const more = currentPage < totalPages; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b8bebe1894f..ead1b8f99d3 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,437 +1,437 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ -(function() { - (function(w) { - var base; - const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; - w.gl || (w.gl = {}); - (base = w.gl).utils || (base.utils = {}); - w.gl.utils.isInGroupsPage = function() { - return gl.utils.getPagePath() === 'groups'; - }; - w.gl.utils.isInProjectPage = function() { - return gl.utils.getPagePath() === 'projects'; - }; - w.gl.utils.getProjectSlug = function() { - if (this.isInProjectPage()) { - return $('body').data('project'); - } else { - return null; - } - }; - w.gl.utils.getGroupSlug = function() { - if (this.isInGroupsPage()) { - return $('body').data('group'); - } else { - return null; - } - }; - - w.gl.utils.isInIssuePage = () => { - const page = gl.utils.getPagePath(1); - const action = gl.utils.getPagePath(2); - - return page === 'issues' && action === 'show'; - }; - w.gl.utils.ajaxGet = function(url) { - return $.ajax({ - type: "GET", - url: url, - dataType: "script" - }); - }; - - w.gl.utils.ajaxPost = function(url, data) { - return $.ajax({ - type: 'POST', - url: url, - data: data, - }); - }; - - w.gl.utils.extractLast = function(term) { - return this.split(term).pop(); - }; - - w.gl.utils.rstrip = function rstrip(val) { - if (val) { - return val.replace(/\s+$/, ''); +export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; + +export const isInGroupsPage = () => getPagePath() === 'groups'; + +export const isInProjectPage = () => getPagePath() === 'projects'; + +export const getProjectSlug = () => { + if (isInProjectPage()) { + return $('body').data('project'); + } + return null; +}; + +export const getGroupSlug = () => { + if (isInGroupsPage()) { + return $('body').data('group'); + } + return null; +}; + +export const isInIssuePage = () => { + const page = getPagePath(1); + const action = getPagePath(2); + + return page === 'issues' && action === 'show'; +}; + +export const ajaxGet = url => $.ajax({ + type: 'GET', + url, + dataType: 'script', +}); + +export const ajaxPost = (url, data) => $.ajax({ + type: 'POST', + url, + data, +}); + +export const rstrip = (val) => { + if (val) { + return val.replace(/\s+$/, ''); + } + return val; +}; + +export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); + +export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { + const field = $(fieldSelector); + const closestSubmit = field.closest('form').find(buttonSelector); + if (rstrip(field.val()) === '') { + closestSubmit.disable(); + } + // eslint-disable-next-line func-names + return field.on(eventName, function () { + if (rstrip($(this).val()) === '') { + return closestSubmit.disable(); + } + return closestSubmit.enable(); + }); +}; + +// automatically adjust scroll position for hash urls taking the height of the navbar into account +// https://github.com/twitter/bootstrap/issues/1768 +export const handleLocationHash = () => { + let hash = window.gl.utils.getLocationHash(); + if (!hash) return; + + // This is required to handle non-unicode characters in hash + hash = decodeURIComponent(hash); + + const fixedTabs = document.querySelector('.js-tabs-affix'); + const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); + const fixedNav = document.querySelector('.navbar-gitlab'); + + let adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + + // scroll to user-generated markdown anchor if we cannot find a match + if (document.getElementById(hash) === null) { + const target = document.getElementById(`user-content-${hash}`); + if (target && target.scrollIntoView) { + target.scrollIntoView(true); + window.scrollBy(0, adjustment); + } + } else { + // only adjust for fixedTabs when not targeting user-generated content + if (fixedTabs) { + adjustment -= fixedTabs.offsetHeight; + } + + if (fixedDiffStats) { + adjustment -= fixedDiffStats.offsetHeight; + } + + window.scrollBy(0, adjustment); + } +}; + +// Check if element scrolled into viewport from above or below +// Courtesy http://stackoverflow.com/a/7557433/414749 +export const isInViewport = (el) => { + const rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth + ); +}; + +export const parseUrl = (url) => { + const parser = document.createElement('a'); + parser.href = url; + return parser; +}; + +export const parseUrlPathname = (url) => { + const parsedUrl = parseUrl(url); + // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 + // We have to make sure we always have an absolute path. + return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; +}; + +// We can trust that each param has one & since values containing & will be encoded +// Remove the first character of search as it is always ? +export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); +}); + +export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; + +// Identify following special clicks +// 1) Cmd + Click on Mac (e.metaKey) +// 2) Ctrl + Click on PC (e.ctrlKey) +// 3) Middle-click or Mouse Wheel Click (e.which is 2) +export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; + +export const scrollToElement = ($el) => { + const top = $el.offset().top; + const mrTabsHeight = $('.merge-request-tabs').height() || 0; + const headerHeight = $('.navbar-gitlab').height() || 0; + + return $('body, html').animate({ + scrollTop: top - mrTabsHeight - headerHeight, + }, 200); +}; + +/** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided +*/ +export const getParameterByName = (name, urlToParse) => { + const url = urlToParse || window.location.href; + const parsedName = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +}; + +export const getSelectedFragment = () => { + const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; + const documentFragment = document.createDocumentFragment(); + for (let i = 0; i < selection.rangeCount; i += 1) { + documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); + } + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; +}; + +// TODO: Update this name, there is a gl.text.insertText function. +export const insertText = (target, text) => { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas + const selectionStart = target.selectionStart; + const selectionEnd = target.selectionEnd; + const value = target.value; + + const textBefore = value.substring(0, selectionStart); + const textAfter = value.substring(selectionEnd, value.length); + + const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; + const newText = textBefore + insertedText + textAfter; + + // eslint-disable-next-line no-param-reassign + target.value = newText; + // eslint-disable-next-line no-param-reassign + target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; + + // Trigger autosave + $(target).trigger('input'); + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + target.dispatchEvent(event); +}; + +export const nodeMatchesSelector = (node, selector) => { + const matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + // eslint-disable-next-line no-param-reassign + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + const matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; +}; + +/** + this will take in the headers from an API response and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeHeaders = (headers) => { + const upperCaseHeaders = {}; + + Object.keys(headers).forEach((e) => { + upperCaseHeaders[e.toUpperCase()] = headers[e]; + }); + + return upperCaseHeaders; +}; + +/** + this will take in the getAllResponseHeaders result and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys +*/ +export const normalizeCRLFHeaders = (headers) => { + const headersObject = {}; + const headersArray = headers.split('\n'); + + headersArray.forEach((header) => { + const keyValue = header.split(': '); + headersObject[keyValue[0]] = keyValue[1]; + }); + + return normalizeHeaders(headersObject); +}; + +/** + * Parses pagination object string values into numbers. + * + * @param {Object} paginationInformation + * @returns {Object} + */ +export const parseIntPagination = paginationInformation => ({ + perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), + page: parseInt(paginationInformation['X-PAGE'], 10), + total: parseInt(paginationInformation['X-TOTAL'], 10), + totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), + nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), + previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), +}); + +/** + * Updates the search parameter of a URL given the parameter and value provided. + * + * If no search params are present we'll add it. + * If param for page is already present, we'll update it + * If there are params but not for the given one, we'll add it at the end. + * Returns the new search parameters. + * + * @param {String} param + * @param {Number|String|Undefined|Null} value + * @return {String} + */ +export const setParamInURL = (param, value) => { + let search; + const locationSearch = window.location.search; + + if (locationSearch.length) { + const parameters = locationSearch.substring(1, locationSearch.length) + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + // eslint-disable-next-line no-param-reassign + acc[val[0]] = decodeURIComponent(val[1]); + return acc; + }, {}); + + parameters[param] = value; + + const toString = Object.keys(parameters) + .map(val => `${val}=${encodeURIComponent(parameters[val])}`) + .join('&'); + + search = `?${toString}`; + } else { + search = `?${param}=${value}`; + } + + return search; +}; + +/** + * Converts permission provided as strings to booleans. + * + * @param {String} string + * @returns {Boolean} + */ +export const convertPermissionToBoolean = permission => permission === 'true'; + +/** + * Back Off exponential algorithm + * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> + * + * @param {Function<next, stop>} fn function to be called + * @param {Number} timeout + * @return {Promise<Any, Error>} + * @example + * ``` + * backOff(function (next, stop) { + * // Let's perform this function repeatedly for 60s or for the timeout provided. + * + * ourFunction() + * .then(function (result) { + * // continue if result is not what we need + * next(); + * + * // when result is what we need let's stop with the repetions and jump out of the cycle + * stop(result); + * }) + * .catch(function (error) { + * // if there is an error, we need to stop this with an error. + * stop(error); + * }) + * }, 60000) + * .then(function (result) {}) + * .catch(function (error) { + * // deal with errors passed to stop() + * }) + * ``` + */ +export const backOff = (fn, timeout = 60000) => { + const maxInterval = 32000; + let nextInterval = 2000; + let timeElapsed = 0; + + return new Promise((resolve, reject) => { + const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); + + const next = () => { + if (timeElapsed < timeout) { + setTimeout(() => fn(next, stop), nextInterval); + timeElapsed += nextInterval; + nextInterval = Math.min(nextInterval + nextInterval, maxInterval); } else { - return val; - } - }; - - gl.utils.updateTooltipTitle = function($tooltipEl, newTitle) { - return $tooltipEl.attr('title', newTitle).tooltip('fixTitle'); - }; - - w.gl.utils.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { - event_name = event_name || 'input'; - var closest_submit, field, that; - that = this; - field = $(field_selector); - closest_submit = field.closest('form').find(button_selector); - if (this.rstrip(field.val()) === "") { - closest_submit.disable(); + reject(new Error('BACKOFF_TIMEOUT')); } - return field.on(event_name, function() { - if (that.rstrip($(this).val()) === "") { - return closest_submit.disable(); - } else { - return closest_submit.enable(); - } - }); }; - // automatically adjust scroll position for hash urls taking the height of the navbar into account - // https://github.com/twitter/bootstrap/issues/1768 - w.gl.utils.handleLocationHash = function() { - var hash = w.gl.utils.getLocationHash(); - if (!hash) return; - - // This is required to handle non-unicode characters in hash - hash = decodeURIComponent(hash); - - const fixedTabs = document.querySelector('.js-tabs-affix'); - const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck'); - const fixedNav = document.querySelector('.navbar-gitlab'); - - var adjustment = 0; - if (fixedNav) adjustment -= fixedNav.offsetHeight; - - // scroll to user-generated markdown anchor if we cannot find a match - if (document.getElementById(hash) === null) { - var target = document.getElementById('user-content-' + hash); - if (target && target.scrollIntoView) { - target.scrollIntoView(true); - window.scrollBy(0, adjustment); - } + fn(next, stop); + }); +}; + +export const setFavicon = (faviconPath) => { + const faviconEl = document.getElementById('favicon'); + if (faviconEl && faviconPath) { + faviconEl.setAttribute('href', faviconPath); + } +}; + +export const resetFavicon = () => { + const faviconEl = document.getElementById('favicon'); + const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + faviconEl.setAttribute('href', originalFavicon); + } +}; + +export const setCiStatusFavicon = (pageUrl) => { + $.ajax({ + url: pageUrl, + dataType: 'json', + success: (data) => { + if (data && data.favicon) { + setFavicon(data.favicon); } else { - // only adjust for fixedTabs when not targeting user-generated content - if (fixedTabs) { - adjustment -= fixedTabs.offsetHeight; - } - - if (fixedDiffStats) { - adjustment -= fixedDiffStats.offsetHeight; - } - - window.scrollBy(0, adjustment); + resetFavicon(); } - }; - - // Check if element scrolled into viewport from above or below - // Courtesy http://stackoverflow.com/a/7557433/414749 - w.gl.utils.isInViewport = function(el) { - var rect = el.getBoundingClientRect(); - - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth - ); - }; - - gl.utils.getPagePath = function(index) { - index = index || 0; - return $('body').data('page').split(':')[index]; - }; - - gl.utils.parseUrl = function (url) { - var parser = document.createElement('a'); - parser.href = url; - return parser; - }; - - gl.utils.parseUrlPathname = function (url) { - var parsedUrl = gl.utils.parseUrl(url); - // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 - // We have to make sure we always have an absolute path. - return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; - }; - - gl.utils.getUrlParamsArray = function () { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&').map((param) => { - const split = param.split('='); - return [decodeURI(split[0]), split[1]].join('='); - }); - }; - - gl.utils.isMetaKey = function(e) { - return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; - }; - - gl.utils.isMetaClick = function(e) { - // Identify following special clicks - // 1) Cmd + Click on Mac (e.metaKey) - // 2) Ctrl + Click on PC (e.ctrlKey) - // 3) Middle-click or Mouse Wheel Click (e.which is 2) - return e.metaKey || e.ctrlKey || e.which === 2; - }; - - gl.utils.scrollToElement = function($el) { - const top = $el.offset().top; - const mrTabsHeight = $('.merge-request-tabs').height() || 0; - const headerHeight = $('.navbar-gitlab').height() || 0; - - return $('body, html').animate({ - scrollTop: top - mrTabsHeight - headerHeight, - }, 200); - }; - - /** - this will take in the `name` of the param you want to parse in the url - if the name does not exist this function will return `null` - otherwise it will return the value of the param key provided - */ - w.gl.utils.getParameterByName = (name, parseUrl) => { - const url = parseUrl || window.location.href; - name = name.replace(/[[\]]/g, '\\$&'); - const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); - const results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - - w.gl.utils.getSelectedFragment = () => { - const selection = window.getSelection(); - if (selection.rangeCount === 0) return null; - const documentFragment = document.createDocumentFragment(); - for (let i = 0; i < selection.rangeCount; i += 1) { - documentFragment.appendChild(selection.getRangeAt(i).cloneContents()); - } - if (documentFragment.textContent.length === 0) return null; - - return documentFragment; - }; - - w.gl.utils.insertText = (target, text) => { - // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas - - const selectionStart = target.selectionStart; - const selectionEnd = target.selectionEnd; - const value = target.value; - - const textBefore = value.substring(0, selectionStart); - const textAfter = value.substring(selectionEnd, value.length); - - const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; - const newText = textBefore + insertedText + textAfter; - - target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; - - // Trigger autosave - $(target).trigger('input'); - - // Trigger autosize - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - target.dispatchEvent(event); - }; - - w.gl.utils.nodeMatchesSelector = (node, selector) => { - const matches = Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector; - - if (matches) { - return matches.call(node, selector); - } - - // IE11 doesn't support `node.matches(selector)` - - let parentNode = node.parentNode; - if (!parentNode) { - parentNode = document.createElement('div'); - node = node.cloneNode(true); - parentNode.appendChild(node); - } - - const matchingNodes = parentNode.querySelectorAll(selector); - return Array.prototype.indexOf.call(matchingNodes, node) !== -1; - }; - - /** - this will take in the headers from an API response and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeHeaders = (headers) => { - const upperCaseHeaders = {}; - - Object.keys(headers).forEach((e) => { - upperCaseHeaders[e.toUpperCase()] = headers[e]; - }); - - return upperCaseHeaders; - }; - - /** - this will take in the getAllResponseHeaders result and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys - */ - w.gl.utils.normalizeCRLFHeaders = (headers) => { - const headersObject = {}; - const headersArray = headers.split('\n'); - - headersArray.forEach((header) => { - const keyValue = header.split(': '); - headersObject[keyValue[0]] = keyValue[1]; - }); - - return w.gl.utils.normalizeHeaders(headersObject); - }; - - /** - * Parses pagination object string values into numbers. - * - * @param {Object} paginationInformation - * @returns {Object} - */ - w.gl.utils.parseIntPagination = paginationInformation => ({ - perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), - page: parseInt(paginationInformation['X-PAGE'], 10), - total: parseInt(paginationInformation['X-TOTAL'], 10), - totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10), - nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10), - previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), - }); - - /** - * Updates the search parameter of a URL given the parameter and value provided. - * - * If no search params are present we'll add it. - * If param for page is already present, we'll update it - * If there are params but not for the given one, we'll add it at the end. - * Returns the new search parameters. - * - * @param {String} param - * @param {Number|String|Undefined|Null} value - * @return {String} - */ - w.gl.utils.setParamInURL = (param, value) => { - let search; - const locationSearch = window.location.search; - - if (locationSearch.length) { - const parameters = locationSearch.substring(1, locationSearch.length) - .split('&') - .reduce((acc, element) => { - const val = element.split('='); - acc[val[0]] = decodeURIComponent(val[1]); - return acc; - }, {}); - - parameters[param] = value; - - const toString = Object.keys(parameters) - .map(val => `${val}=${encodeURIComponent(parameters[val])}`) - .join('&'); - - search = `?${toString}`; - } else { - search = `?${param}=${value}`; - } - - return search; - }; - - /** - * Converts permission provided as strings to booleans. - * - * @param {String} string - * @returns {Boolean} - */ - w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; - - /** - * Back Off exponential algorithm - * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> - * - * @param {Function<next, stop>} fn function to be called - * @param {Number} timeout - * @return {Promise<Any, Error>} - * @example - * ``` - * backOff(function (next, stop) { - * // Let's perform this function repeatedly for 60s or for the timeout provided. - * - * ourFunction() - * .then(function (result) { - * // continue if result is not what we need - * next(); - * - * // when result is what we need let's stop with the repetions and jump out of the cycle - * stop(result); - * }) - * .catch(function (error) { - * // if there is an error, we need to stop this with an error. - * stop(error); - * }) - * }, 60000) - * .then(function (result) {}) - * .catch(function (error) { - * // deal with errors passed to stop() - * }) - * ``` - */ - w.gl.utils.backOff = (fn, timeout = 60000) => { - const maxInterval = 32000; - let nextInterval = 2000; - let timeElapsed = 0; - - return new Promise((resolve, reject) => { - const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); - - const next = () => { - if (timeElapsed < timeout) { - setTimeout(() => fn(next, stop), nextInterval); - timeElapsed += nextInterval; - nextInterval = Math.min(nextInterval + nextInterval, maxInterval); - } else { - reject(new Error('BACKOFF_TIMEOUT')); - } - }; - - fn(next, stop); - }); - }; - - w.gl.utils.setFavicon = (faviconPath) => { - if (faviconEl && faviconPath) { - faviconEl.setAttribute('href', faviconPath); - } - }; - - w.gl.utils.resetFavicon = () => { - if (faviconEl) { - faviconEl.setAttribute('href', originalFavicon); - } - }; - - w.gl.utils.setCiStatusFavicon = (pageUrl) => { - $.ajax({ - url: pageUrl, - dataType: 'json', - success: function(data) { - if (data && data.favicon) { - gl.utils.setFavicon(data.favicon); - } else { - gl.utils.resetFavicon(); - } - }, - error: function() { - gl.utils.resetFavicon(); - } - }); - }; - })(window); -}).call(window); + }, + error: () => { + resetFavicon(); + }, + }); +}; + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + getPagePath, + isInGroupsPage, + isInProjectPage, + getProjectSlug, + getGroupSlug, + isInIssuePage, + ajaxGet, + ajaxPost, + rstrip, + updateTooltipTitle, + disableButtonIfEmptyField, + handleLocationHash, + isInViewport, + parseUrl, + parseUrlPathname, + getUrlParamsArray, + isMetaKey, + isMetaClick, + scrollToElement, + getParameterByName, + getSelectedFragment, + insertText, + nodeMatchesSelector, +}; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 97666e13ebe..1485e900945 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -1,4 +1,5 @@ import httpStatusCodes from './http_status'; +import { normalizeHeaders } from './common_utils'; /** * Polling utility for handling realtime updates. @@ -57,7 +58,7 @@ export default class Poll { } checkConditions(response) { - const headers = gl.utils.normalizeHeaders(response.headers); + const headers = normalizeHeaders(response.headers); const pollInterval = parseInt(headers[this.intervalHeader], 10); if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c2a104df749..58d877f73fe 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -40,7 +40,7 @@ import './commit/image_file'; // lib/utils import './lib/utils/bootstrap_linked_tabs'; -import './lib/utils/common_utils'; +import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; import './lib/utils/pretty_time'; import './lib/utils/text_utility'; @@ -160,10 +160,10 @@ document.addEventListener('beforeunload', function () { $('[data-toggle="popover"]').popover('destroy'); }); -window.addEventListener('hashchange', gl.utils.handleLocationHash); +window.addEventListener('hashchange', handleLocationHash); window.addEventListener('load', function onLoad() { window.removeEventListener('load', onLoad, false); - gl.utils.handleLocationHash(); + handleLocationHash(); }, false); gl.lazyLoader = new LazyLoader({ @@ -189,7 +189,7 @@ $(function () { $body.on('click', 'a[href^="#"]', function() { var href = this.getAttribute('href'); if (href.substr(1) === gl.utils.getLocationHash()) { - setTimeout(gl.utils.handleLocationHash, 1); + setTimeout(handleLocationHash, 1); } }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 0c3c877ff15..f71a59aaa84 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -7,6 +7,11 @@ import './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; +import { + parseUrlPathname, + handleLocationHash, + isMetaClick, +} from './lib/utils/common_utils'; /* eslint-disable max-len */ // MergeRequestTabs @@ -114,7 +119,7 @@ import bp from './breakpoints'; } clickTab(e) { - if (e.currentTarget && gl.utils.isMetaClick(e)) { + if (e.currentTarget && isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); e.stopImmediatePropagation(); e.preventDefault(); @@ -260,7 +265,7 @@ import bp from './breakpoints'; // We extract pathname for the current Changes tab anchor href // some pages like MergeRequestsController#new has query parameters on that anchor - const urlPathname = gl.utils.parseUrlPathname(source); + const urlPathname = parseUrlPathname(source); this.ajaxGet({ url: `${urlPathname}.json${location.search}`, @@ -309,7 +314,7 @@ import bp from './breakpoints'; forceShow: true, }); anchor[0].scrollIntoView(); - window.gl.utils.handleLocationHash(); + handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 5d96b193fce..192473b7dd1 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -7,6 +7,7 @@ import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; import eventHub from '../event_hub'; + import { convertPermissionToBoolean } from '../../lib/utils/common_utils'; export default { @@ -17,7 +18,7 @@ return { store, state: 'gettingStarted', - hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), + hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, metricsEndpoint: metricsData.additionalMetrics, diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 4ed651d5740..fed884d5c94 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import statusCodes from '../../lib/utils/http_status'; +import { backOff } from '../../lib/utils/common_utils'; Vue.use(VueResource); @@ -8,7 +9,7 @@ const MAX_REQUESTS = 3; function backOffRequest(makeRequestCallback) { let requestCounter = 0; - return gl.utils.backOff((next, stop) => { + return backOff((next, stop) => { makeRequestCallback().then((resp) => { if (resp.status === statusCodes.NO_CONTENT) { requestCounter += 1; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index f5f7bb4653d..93aa29454a0 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -23,6 +23,7 @@ import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; import TaskList from './task_list'; +import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; window.autosize = autosize; window.Dropzone = Dropzone; @@ -81,7 +82,7 @@ export default class Notes { this.setViewType(view); // We are in the Merge Requests page so we need another edit form for Changes tab - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { $('.note-edit-form').clone() .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); } @@ -175,7 +176,7 @@ export default class Notes { keydownNoteText(e) { var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (gl.utils.isMetaKey(e)) { + if (isMetaKey(e)) { return; } @@ -644,10 +645,10 @@ export default class Notes { } else { var $buttons = $el.find('.note-form-actions'); - var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + var isWidgetVisible = isInViewport($el.get(0)); if (!isWidgetVisible) { - gl.utils.scrollToElement($el); + scrollToElement($el); } $el.find('.js-finish-edit-warning').show(); @@ -1188,7 +1189,7 @@ export default class Notes { } static checkMergeRequestStatus() { - if (gl.utils.getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests') { gl.mrWidget.checkStatus(); } } @@ -1326,7 +1327,7 @@ export default class Notes { * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 3) Build temporary placeholder element (using `createPlaceholderNote`) * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `gl.utils.ajaxPost` + * 5) Perform network request to submit the note using `ajaxPost` * a) If request is successfully completed * 1. Remove placeholder element * 2. Show submitted Note element @@ -1408,7 +1409,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to submit comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! remove placeholder $notesContainer.find(`#${noteUniqueId}`).remove(); @@ -1481,7 +1482,7 @@ export default class Notes { * * 1) Get Form metadata * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` + * 3) Perform network request to submit the updated note using `ajaxPost` * a) If request is successfully completed * 1. Show submitted Note element * b) If request failed @@ -1510,7 +1511,7 @@ export default class Notes { /* eslint-disable promise/catch-or-return */ // Make request to update comment on server - gl.utils.ajaxPost(formAction, formData) + ajaxPost(formAction, formData) .then((note) => { // Submission successful! render final note element this.updateNote(note, $editingNote); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 13cd74bfa1c..923611bda9a 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -7,6 +7,7 @@ import * as constants from '../constants'; import service from '../services/issue_notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; +import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; let eTagPoll; @@ -211,7 +212,7 @@ export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { }; export const scrollToNoteIfNeeded = (context, el) => { - if (!gl.utils.isInViewport(el[0])) { - gl.utils.scrollToElement(el); + if (!isInViewport(el[0])) { + scrollToElement(el); } }; diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 01110420cca..e3fc1e2fc2f 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,4 +1,4 @@ -import '~/lib/utils/common_utils'; +import { getParameterByName } from '~/lib/utils/common_utils'; import '~/lib/utils/url_utility'; (() => { @@ -9,7 +9,7 @@ import '~/lib/utils/url_utility'; init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; - this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; + this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; this.prepareData = prepareData; this.callback = callback; diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js index 644efd10509..9e0e5cacb11 100644 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -1,3 +1,5 @@ +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; + function insertRow($row) { const $rowClone = $row.clone(); $rowClone.removeAttr('data-is-persisted'); @@ -6,7 +8,7 @@ function insertRow($row) { } function removeRow($row) { - const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); if (isPersisted) { $row.hide(); diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 26a36ad54d1..07abe714367 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -1,4 +1,5 @@ import LinkedTabs from './lib/utils/bootstrap_linked_tabs'; +import { setCiStatusFavicon } from './lib/utils/common_utils'; export default class Pipelines { constructor(options = {}) { @@ -8,7 +9,7 @@ export default class Pipelines { } if (options.pipelineStatusUrl) { - gl.utils.setCiStatusFavicon(options.pipelineStatusUrl); + setCiStatusFavicon(options.pipelineStatusUrl); } } } diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 5e6d6b2fbdc..4c53e2541fc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -4,6 +4,7 @@ import tablePagination from '../../vue_shared/components/table_pagination.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; + import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; export default { props: { @@ -44,10 +45,10 @@ }, computed: { canCreatePipelineParsed() { - return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + return convertPermissionToBoolean(this.canCreatePipeline); }, scope() { - const scope = gl.utils.getParameterByName('scope'); + const scope = getParameterByName('scope'); return scope === null ? 'all' : scope; }, @@ -105,10 +106,10 @@ }; }, pageParameter() { - return gl.utils.getParameterByName('page') || this.pagenum; + return getParameterByName('page') || this.pagenum; }, scopeParameter() { - return gl.utils.getParameterByName('scope') || this.apiScope; + return getParameterByName('scope') || this.apiScope; }, }, created() { @@ -122,7 +123,7 @@ * @param {Number} pageNumber desired page to go to. */ change(pageNumber) { - const param = gl.utils.setParamInURL('page', pageNumber); + const param = setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/pipelines/stores/pipelines_store.js index ffefe0192f2..651251d2623 100644 --- a/app/assets/javascripts/pipelines/stores/pipelines_store.js +++ b/app/assets/javascripts/pipelines/stores/pipelines_store.js @@ -1,3 +1,5 @@ +import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils'; + export default class PipelinesStore { constructor() { this.state = {}; @@ -19,8 +21,8 @@ export default class PipelinesStore { let paginationInfo; if (Object.keys(pagination).length) { - const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - paginationInfo = gl.utils.parseIntPagination(normalizedHeaders); + const normalizedHeaders = normalizeHeaders(pagination); + paginationInfo = parseIntPagination(normalizedHeaders); } else { paginationInfo = pagination; } diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 4ccea0624ee..3deb242bc1f 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,6 @@ /* 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 { getPagePath } from '../lib/utils/common_utils'; ((global) => { class Profile { @@ -93,7 +94,7 @@ return $title.val(comment[1]).change(); } }); - if (global.utils.getPagePath() === 'profiles') { + if (getPagePath() === 'profiles') { return new Profile(); } }); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index ef4d6df5138..a4d50a52315 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -1,4 +1,5 @@ import PANEL_STATE from './constants'; +import { backOff } from '../lib/utils/common_utils'; export default class PrometheusMetrics { constructor(wrapperSelector) { @@ -79,7 +80,7 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { $.getJSON(this.activeMetricsEndpoint) .done((res) => { if (res && res.success) { diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 003a15592f3..38c9a71dd20 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,4 +1,5 @@ /* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ +import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; ((global) => { const KEYCODE = { @@ -146,14 +147,14 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName; userId = gon.current_user_id; userName = gon.current_username; - utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; - if (utils.isInGroupsPage() && groupOptions) { - options = groupOptions[utils.getGroupSlug()]; - } else if (utils.isInProjectPage() && projectOptions) { - options = projectOptions[utils.getProjectSlug()]; + projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; } else if (dashboardOptions) { options = dashboardOptions; } diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index a606852c22c..2fffe09c74e 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ import UsersSelect from './users_select'; +import { isMetaClick } from './lib/utils/common_utils'; export default class Todos { constructor() { @@ -137,22 +138,17 @@ export default class Todos { goToTodoUrl(e) { const todoLink = this.dataset.url; - if (!todoLink) { + if (!todoLink || e.target.tagName === 'A' || e.target.tagName === 'IMG') { return; } - if (gl.utils.isMetaClick(e)) { + e.stopPropagation(); + e.preventDefault(); + + if (isMetaClick(e)) { const windowTarget = '_blank'; - const selected = e.target; - e.stopPropagation(); - e.preventDefault(); - - if (selected.tagName === 'IMG') { - const avatarUrl = selected.parentElement.getAttribute('href'); - window.open(avatarUrl, windowTarget); - } else { - window.open(todoLink, windowTarget); - } + + window.open(todoLink, windowTarget); } else { gl.utils.visitUrl(todoLink); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index a4e34116c33..a8c686e5065 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -1,6 +1,6 @@ import statusCodes from '../../lib/utils/http_status'; import { bytesToMiB } from '../../lib/utils/number_utils'; - +import { backOff } from '../../lib/utils/common_utils'; import MemoryGraph from '../../vue_shared/components/memory_graph'; import MRWidgetService from '../services/mr_widget_service'; @@ -84,7 +84,7 @@ export default { } }, loadMetrics() { - gl.utils.backOff((next, stop) => { + backOff((next, stop) => { MRWidgetService.fetchMetrics(this.metricsUrl) .then((res) => { if (res.status === statusCodes.NO_CONTENT) { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 2f237262028..044b664484b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -31,6 +31,7 @@ import { SquashBeforeMerge, notify, } from './dependencies'; +import { setFavicon } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -86,7 +87,7 @@ export default { .then((res) => { this.handleNotification(res); this.mr.setData(res); - this.setFavicon(); + this.setFaviconHelper(); if (cb) { cb.call(null, res); @@ -115,9 +116,9 @@ export default { immediateExecution: true, }); }, - setFavicon() { + setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - gl.utils.setFavicon(this.mr.ciStatusFaviconPath); + setFavicon(this.mr.ciStatusFaviconPath); } }, fetchDeployments() { @@ -193,7 +194,7 @@ export default { }); }, handleMounted() { - this.setFavicon(); + this.setFaviconHelper(); this.initDeploymentsPolling(); }, }, diff --git a/changelogs/unreleased/37220-es-modules.yml b/changelogs/unreleased/37220-es-modules.yml new file mode 100644 index 00000000000..de81fa9e74d --- /dev/null +++ b/changelogs/unreleased/37220-es-modules.yml @@ -0,0 +1,5 @@ +--- +title: Exports common_utils utility functions as modules +merge_request: +author: +type: other diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index fdaea5c0b0c..7e62d356bd2 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -14,6 +14,10 @@ describe('Environments Folder View', () => { window.history.pushState({}, null, 'environments/folders/build'); }); + afterEach(() => { + window.history.pushState({}, null, '/'); + }); + let component; describe('successfull request', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index a6ad250bd86..fd5b7d4e5da 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,398 +1,415 @@ /* eslint-disable promise/catch-or-return */ -import '~/lib/utils/common_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; -(() => { - describe('common_utils', () => { - describe('gl.utils.parseUrl', () => { - it('returns an anchor tag with url', () => { - expect(gl.utils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); - }); - it('url is escaped', () => { - // IE11 will return a relative pathname while other browsers will return a full pathname. - // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor - // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/' which will lead to an absolute url - // starting with '/'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); - }); +describe('common_utils', () => { + describe('parseUrl', () => { + it('returns an anchor tag with url', () => { + expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url'); }); + it('url is escaped', () => { + // IE11 will return a relative pathname while other browsers will return a full pathname. + // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor + // element will create an absolute url relative to the current execution context. + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(commonUtils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); + }); + }); - describe('gl.utils.parseUrlPathname', () => { - beforeEach(() => { - spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ - pathname: url, - })); - }); - it('returns an absolute url when given an absolute url', () => { - expect(gl.utils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); - }); - it('returns an absolute url when given a relative url', () => { - expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); - }); + describe('parseUrlPathname', () => { + it('returns an absolute url when given an absolute url', () => { + expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); }); - describe('gl.utils.getUrlParamsArray', () => { - it('should return params array', () => { - expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); - }); + it('returns an absolute url when given a relative url', () => { + expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); + }); + }); - it('should remove the question mark from the search params', () => { - const paramsArray = gl.utils.getUrlParamsArray(); - expect(paramsArray[0][0] !== '?').toBe(true); - }); + describe('getUrlParamsArray', () => { + it('should return params array', () => { + expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true); + }); - it('should decode params', () => { - history.pushState('', '', '?label_name%5B%5D=test'); + it('should remove the question mark from the search params', () => { + const paramsArray = commonUtils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); - expect( - gl.utils.getUrlParamsArray()[0], - ).toBe('label_name[]=test'); + it('should decode params', () => { + history.pushState('', '', '?label_name%5B%5D=test'); - history.pushState('', '', '?'); - }); + expect( + commonUtils.getUrlParamsArray()[0], + ).toBe('label_name[]=test'); + + history.pushState('', '', '?'); }); + }); - describe('gl.utils.handleLocationHash', () => { - beforeEach(() => { - spyOn(window.document, 'getElementById').and.callThrough(); - }); + describe('handleLocationHash', () => { + beforeEach(() => { + spyOn(window.document, 'getElementById').and.callThrough(); + }); - afterEach(() => { - window.history.pushState({}, null, ''); - }); + afterEach(() => { + window.history.pushState({}, null, ''); + }); - function expectGetElementIdToHaveBeenCalledWith(elementId) { - expect(window.document.getElementById).toHaveBeenCalledWith(elementId); - } + function expectGetElementIdToHaveBeenCalledWith(elementId) { + expect(window.document.getElementById).toHaveBeenCalledWith(elementId); + } - it('decodes hash parameter', () => { - window.history.pushState({}, null, '#random-hash'); - gl.utils.handleLocationHash(); + it('decodes hash parameter', () => { + window.history.pushState({}, null, '#random-hash'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('random-hash'); - expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); - }); + expectGetElementIdToHaveBeenCalledWith('random-hash'); + expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); + }); - it('decodes cyrillic hash parameter', () => { - window.history.pushState({}, null, '#definição'); - gl.utils.handleLocationHash(); + it('decodes cyrillic hash parameter', () => { + window.history.pushState({}, null, '#definição'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); + }); - it('decodes encoded cyrillic hash parameter', () => { - window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); - gl.utils.handleLocationHash(); + it('decodes encoded cyrillic hash parameter', () => { + window.history.pushState({}, null, '#defini%C3%A7%C3%A3o'); + commonUtils.handleLocationHash(); - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); + expectGetElementIdToHaveBeenCalledWith('definição'); + expectGetElementIdToHaveBeenCalledWith('user-content-definição'); }); + }); - describe('gl.utils.setParamInURL', () => { - afterEach(() => { - window.history.pushState({}, null, ''); - }); + describe('setParamInURL', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); - it('should return the parameter', () => { - window.history.replaceState({}, null, ''); + it('should return the parameter', () => { + window.history.replaceState({}, null, ''); - expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156'); - expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156'); - }); + expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156'); + expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156'); + }); - it('should update the existing parameter when its a number', () => { - window.history.pushState({}, null, '?page=15'); + it('should update the existing parameter when its a number', () => { + window.history.pushState({}, null, '?page=15'); - expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?page=true'); - }); + expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?page=true'); + }); - it('should update the existing parameter when its a string', () => { - window.history.pushState({}, null, '?scope=all'); + it('should update the existing parameter when its a string', () => { + window.history.pushState({}, null, '?scope=all'); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); + }); - it('should update the existing parameter when more than one parameter exists', () => { - window.history.pushState({}, null, '?scope=all&page=15'); + it('should update the existing parameter when more than one parameter exists', () => { + window.history.pushState({}, null, '?scope=all&page=15'); - expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); - }); + expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); + }); - it('should add a new parameter to the end of the existing ones', () => { - window.history.pushState({}, null, '?scope=all'); + it('should add a new parameter to the end of the existing ones', () => { + window.history.pushState({}, null, '?scope=all'); - expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); - expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true'); - }); + expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); + expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true'); }); + }); - describe('gl.utils.getParameterByName', () => { - beforeEach(() => { - window.history.pushState({}, null, '?scope=all&p=2'); - }); + describe('getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); - afterEach(() => { - window.history.replaceState({}, null, null); - }); + afterEach(() => { + window.history.replaceState({}, null, null); + }); - it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('scope'); - expect(gl.utils.getParameterByName('p')).toEqual('2'); - expect(value).toBe('all'); - }); + it('should return valid parameter', () => { + const value = commonUtils.getParameterByName('scope'); + expect(commonUtils.getParameterByName('p')).toEqual('2'); + expect(value).toBe('all'); + }); - it('should return invalid parameter', () => { - const value = gl.utils.getParameterByName('fakeParameter'); - expect(value).toBe(null); - }); + it('should return invalid parameter', () => { + const value = commonUtils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); - it('should return valid paramentes if URL is provided', () => { - let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); - expect(value).toBe('bar'); + it('should return valid paramentes if URL is provided', () => { + let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); + expect(value).toBe('bar'); - value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); - expect(value).toBe('canchu'); - }); + value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); + expect(value).toBe('canchu'); }); + }); - describe('gl.utils.normalizedHeaders', () => { - it('should upperCase all the header keys to keep them consistent', () => { - const apiHeaders = { - 'X-Something-Workhorse': { workhorse: 'ok' }, - 'x-something-nginx': { nginx: 'ok' }, - }; + describe('normalizedHeaders', () => { + it('should upperCase all the header keys to keep them consistent', () => { + const apiHeaders = { + 'X-Something-Workhorse': { workhorse: 'ok' }, + 'x-something-nginx': { nginx: 'ok' }, + }; - const normalized = gl.utils.normalizeHeaders(apiHeaders); + const normalized = commonUtils.normalizeHeaders(apiHeaders); - const WORKHORSE = 'X-SOMETHING-WORKHORSE'; - const NGINX = 'X-SOMETHING-NGINX'; + const WORKHORSE = 'X-SOMETHING-WORKHORSE'; + const NGINX = 'X-SOMETHING-NGINX'; - expect(normalized[WORKHORSE].workhorse).toBe('ok'); - expect(normalized[NGINX].nginx).toBe('ok'); - }); + expect(normalized[WORKHORSE].workhorse).toBe('ok'); + expect(normalized[NGINX].nginx).toBe('ok'); }); + }); - describe('gl.utils.normalizeCRLFHeaders', () => { - beforeEach(function () { - this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + describe('normalizeCRLFHeaders', () => { + beforeEach(function () { + this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + spyOn(String.prototype, 'split').and.callThrough(); + this.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(this.CLRFHeaders); + }); - spyOn(String.prototype, 'split').and.callThrough(); - spyOn(gl.utils, 'normalizeHeaders').and.callThrough(); + it('should split by newline', function () { + expect(String.prototype.split).toHaveBeenCalledWith('\n'); + }); - this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders); - }); + it('should split by colon+space for each header', function () { + expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); + }); - it('should split by newline', function () { - expect(String.prototype.split).toHaveBeenCalledWith('\n'); + it('should return a normalized headers object', function () { + expect(this.normalizeCRLFHeaders).toEqual({ + 'A-HEADER': 'a-value', + 'ANOTHER-HEADER': 'ANOTHER-VALUE', + 'LAST-HEADER': 'last-VALUE', }); + }); + }); - it('should split by colon+space for each header', function () { - expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3); - }); + describe('parseIntPagination', () => { + it('should parse to integers all string values and return pagination object', () => { + const pagination = { + 'X-PER-PAGE': 10, + 'X-PAGE': 2, + 'X-TOTAL': 30, + 'X-TOTAL-PAGES': 3, + 'X-NEXT-PAGE': 3, + 'X-PREV-PAGE': 1, + }; + + const expectedPagination = { + perPage: 10, + page: 2, + total: 30, + totalPages: 3, + nextPage: 3, + previousPage: 1, + }; + + expect(commonUtils.parseIntPagination(pagination)).toEqual(expectedPagination); + }); + }); - it('should call gl.utils.normalizeHeaders with a parsed headers object', function () { - expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object)); - }); + describe('isMetaClick', () => { + it('should identify meta click on Windows/Linux', () => { + const e = { + metaKey: false, + ctrlKey: true, + which: 1, + }; - it('should return a normalized headers object', function () { - expect(this.normalizeCRLFHeaders).toEqual({ - 'A-HEADER': 'a-value', - 'ANOTHER-HEADER': 'ANOTHER-VALUE', - 'LAST-HEADER': 'last-VALUE', - }); - }); + expect(commonUtils.isMetaClick(e)).toBe(true); }); - describe('gl.utils.parseIntPagination', () => { - it('should parse to integers all string values and return pagination object', () => { - const pagination = { - 'X-PER-PAGE': 10, - 'X-PAGE': 2, - 'X-TOTAL': 30, - 'X-TOTAL-PAGES': 3, - 'X-NEXT-PAGE': 3, - 'X-PREV-PAGE': 1, - }; - - const expectedPagination = { - perPage: 10, - page: 2, - total: 30, - totalPages: 3, - nextPage: 3, - previousPage: 1, - }; - - expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination); - }); - }); + it('should identify meta click on macOS', () => { + const e = { + metaKey: true, + ctrlKey: false, + which: 1, + }; - describe('gl.utils.isMetaClick', () => { - it('should identify meta click on Windows/Linux', () => { - const e = { - metaKey: false, - ctrlKey: true, - which: 1, - }; + expect(commonUtils.isMetaClick(e)).toBe(true); + }); - expect(gl.utils.isMetaClick(e)).toBe(true); - }); + it('should identify as meta click on middle-click or Mouse-wheel click', () => { + const e = { + metaKey: false, + ctrlKey: false, + which: 2, + }; - it('should identify meta click on macOS', () => { - const e = { - metaKey: true, - ctrlKey: false, - which: 1, - }; + expect(commonUtils.isMetaClick(e)).toBe(true); + }); + }); - expect(gl.utils.isMetaClick(e)).toBe(true); - }); + describe('convertPermissionToBoolean', () => { + it('should convert a boolean in a string to a boolean', () => { + expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true); + expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false); + }); + }); - it('should identify as meta click on middle-click or Mouse-wheel click', () => { - const e = { - metaKey: false, - ctrlKey: false, - which: 2, - }; + describe('backOff', () => { + beforeEach(() => { + // shortcut our timeouts otherwise these tests will take a long time to finish + const origSetTimeout = window.setTimeout; + spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + }); - expect(gl.utils.isMetaClick(e)).toBe(true); + it('solves the promise from the callback', (done) => { + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + new Promise((resolve) => { + resolve(expectedResponseValue); + }).then((resp) => { + stop(resp); + }) + )).then((respBackoff) => { + expect(respBackoff).toBe(expectedResponseValue); + done(); }); }); - describe('gl.utils.backOff', () => { - beforeEach(() => { - // shortcut our timeouts otherwise these tests will take a long time to finish - const origSetTimeout = window.setTimeout; - spyOn(window, 'setTimeout').and.callFake(cb => origSetTimeout(cb, 0)); + it('catches the rejected promise from the callback ', (done) => { + const errorMessage = 'Mistakes were made!'; + commonUtils.backOff((next, stop) => { + new Promise((resolve, reject) => { + reject(new Error(errorMessage)); + }).then((resp) => { + stop(resp); + }).catch(err => stop(err)); + }).catch((errBackoffResp) => { + expect(errBackoffResp instanceof Error).toBe(true); + expect(errBackoffResp.message).toBe(errorMessage); + done(); }); + }); - it('solves the promise from the callback', (done) => { - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - new Promise((resolve) => { - resolve(expectedResponseValue); - }).then((resp) => { - stop(resp); + it('solves the promise correctly after retrying a third time', (done) => { + let numberOfCalls = 1; + const expectedResponseValue = 'Success!'; + commonUtils.backOff((next, stop) => ( + Promise.resolve(expectedResponseValue) + .then((resp) => { + if (numberOfCalls < 3) { + numberOfCalls += 1; + next(); + } else { + stop(resp); + } }) - )).then((respBackoff) => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); + )).then((respBackoff) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000]); + expect(respBackoff).toBe(expectedResponseValue); + done(); }); + }); - it('catches the rejected promise from the callback ', (done) => { - const errorMessage = 'Mistakes were made!'; - gl.utils.backOff((next, stop) => { - new Promise((resolve, reject) => { - reject(new Error(errorMessage)); - }).then((resp) => { - stop(resp); - }).catch(err => stop(err)); - }).catch((errBackoffResp) => { + it('rejects the backOff promise after timing out', (done) => { + commonUtils.backOff(next => next(), 64000) + .catch((errBackoffResp) => { + const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); + expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe(errorMessage); + expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); done(); }); - }); + }); + }); - it('solves the promise correctly after retrying a third time', (done) => { - let numberOfCalls = 1; - const expectedResponseValue = 'Success!'; - gl.utils.backOff((next, stop) => ( - Promise.resolve(expectedResponseValue) - .then((resp) => { - if (numberOfCalls < 3) { - numberOfCalls += 1; - next(); - } else { - stop(resp); - } - }) - )).then((respBackoff) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }); - }); + describe('setFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); + }); - it('rejects the backOff promise after timing out', (done) => { - gl.utils.backOff(next => next(), 64000) - .catch((errBackoffResp) => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - expect(timeouts).toEqual([2000, 4000, 8000, 16000, 32000, 32000]); - expect(errBackoffResp instanceof Error).toBe(true); - expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT'); - done(); - }); - }); + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); }); + it('should set page favicon to provided favicon', () => { + const faviconPath = '//custom_favicon'; + commonUtils.setFavicon(faviconPath); - describe('gl.utils.setFavicon', () => { - it('should set page favicon to provided favicon', () => { - const faviconPath = '//custom_favicon'; - const fakeLink = { - setAttribute() {}, - }; + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconPath); + }); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val.indexOf(faviconPath) > -1).toBe(true); - }); - gl.utils.setFavicon(faviconPath); - }); + describe('resetFavicon', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'default/favicon'); + document.body.appendChild(favicon); }); - describe('gl.utils.resetFavicon', () => { - it('should reset page favicon to tanuki', () => { - const fakeLink = { - setAttribute() {}, - }; + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); - spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); - spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { - expect(attr).toEqual('href'); - expect(val).toMatch(/favicon/); - }); - gl.utils.resetFavicon(); - }); + it('should reset page favicon to tanuki', () => { + commonUtils.resetFavicon(); + expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); }); + }); - describe('gl.utils.setCiStatusFavicon', () => { - it('should set page favicon to CI status favicon based on provided status', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; - const FAVICON_PATH = '//icon_status_success'; - const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); - const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ favicon: FAVICON_PATH }); - expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH); - options.success(); - expect(spyResetFavicon).toHaveBeenCalled(); - options.error(); - expect(spyResetFavicon).toHaveBeenCalled(); - }); + describe('setCiStatusFavicon', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); - gl.utils.setCiStatusFavicon(BUILD_URL); + it('should reset favicon in case of error', () => { + const favicon = document.getElementById('favicon'); + spyOn($, 'ajax').and.callFake(function (options) { + options.error(); + expect(favicon.getAttribute('href')).toEqual('null'); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); }); - describe('gl.utils.ajaxPost', () => { - it('should perform `$.ajax` call and do `POST` request', () => { - const requestURL = '/some/random/api'; - const data = { keyname: 'value' }; - const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + it('should set page favicon to CI status favicon based on provided status', () => { + const FAVICON_PATH = '//icon_status_success'; + const favicon = document.getElementById('favicon'); - gl.utils.ajaxPost(requestURL, data); - expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ favicon: FAVICON_PATH }); + expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); }); + + commonUtils.setCiStatusFavicon(BUILD_URL); + }); + }); + + describe('ajaxPost', () => { + it('should perform `$.ajax` call and do `POST` request', () => { + const requestURL = '/some/random/api'; + const data = { keyname: 'value' }; + const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); + + commonUtils.ajaxPost(requestURL, data); + expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); }); }); -})(); +}); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 8830a2d29e5..eadab738376 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -78,8 +78,9 @@ import 'vendor/jquery.scrollTo'; }); describe('meta click', () => { + let metakeyEvent; beforeEach(function () { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); }); it('opens page when commits link is clicked', function () { @@ -89,7 +90,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a').click(); + $('.merge-request-tabs .commits-tab a').trigger(metakeyEvent); }); it('opens page when commits badge is clicked', function () { @@ -99,7 +100,7 @@ import 'vendor/jquery.scrollTo'; }); this.class.bindEvents(); - document.querySelector('.merge-request-tabs .commits-tab a .badge').click(); + $('.merge-request-tabs .commits-tab a .badge').trigger(metakeyEvent); }); }); diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index fd492159081..7d3c9319a11 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -26,37 +26,30 @@ describe('Todos', () => { describe('meta click', () => { let visitUrlSpy; + let windowOpenSpy; + let metakeyEvent; beforeEach(() => { - spyOn(gl.utils, 'isMetaClick').and.returnValue(true); + metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); - it('opens the todo url in another tab', (done) => { + it('opens the todo url in another tab', () => { const todoLink = todoItem.dataset.url; - spyOn(window, 'open').and.callFake((url, target) => { - expect(todoLink).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); + $('.todos-list .todo').trigger(metakeyEvent); - todoItem.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalledWith(todoLink, '_blank'); }); - it('opens the avatar\'s url in another tab when the avatar is clicked', (done) => { - const avatarImage = todoItem.querySelector('img'); - const avatarUrl = avatarImage.parentElement.getAttribute('href'); + it('run native funcionality when avatar is clicked', () => { + $('.todos-list a').on('click', e => e.preventDefault()); + $('.todos-list img').trigger(metakeyEvent); - spyOn(window, 'open').and.callFake((url, target) => { - expect(avatarUrl).toEqual(url); - expect(target).toEqual('_blank'); - done(); - }); - - avatarImage.click(); expect(visitUrlSpy).not.toHaveBeenCalled(); + expect(windowOpenSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index da66c7504cb..e4324e91502 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -224,29 +224,41 @@ describe('mrWidgetOptions', () => { describe('handleMounted', () => { it('should call required methods to do the initial kick-off', () => { spyOn(vm, 'initDeploymentsPolling'); - spyOn(vm, 'setFavicon'); + spyOn(vm, 'setFaviconHelper'); vm.handleMounted(); - expect(vm.setFavicon).toHaveBeenCalled(); + expect(vm.setFaviconHelper).toHaveBeenCalled(); expect(vm.initDeploymentsPolling).toHaveBeenCalled(); }); }); describe('setFavicon', () => { + let faviconElement; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + document.body.appendChild(favicon); + + faviconElement = document.getElementById('favicon'); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + it('should call setFavicon method', () => { - spyOn(gl.utils, 'setFavicon'); - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath); + expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { - spyOn(gl.utils, 'setFavicon'); vm.mr.ciStatusFaviconPath = null; - vm.setFavicon(); + vm.setFaviconHelper(); - expect(gl.utils.setFavicon).not.toHaveBeenCalled(); + expect(faviconElement.getAttribute('href')).toEqual(null); }); }); |