diff options
6 files changed, 127 insertions, 172 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index ccf1d924ef2..afdca012127 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -4,6 +4,14 @@ import { getLocationHash } from './url_utility'; import { convertToCamelCase } from './text_utility'; import { isObject } from './type_utility'; +/** + * Simply returns `window.location`. This function exists to provide a means to spy + * `window.location` in unit tests. + * + * @returns {Location | string | any} The browser's `window.location` + */ +export const windowLocation = () => window.location; + export const getPagePath = (index = 0) => { const page = $('body').attr('data-page') || ''; @@ -180,87 +188,6 @@ export const urlParamsToObject = (path = '') => return data; }, {}); -/** - * Apply the query param and value to the given url by returning a new url string that includes - * the param/value pair. If the given url already includes the query param, the query param value - * will be updated in the new url string. Otherwise, the query param and value will by added in - * the new url string. - * - * @param url {string} - url to which the query param will be applied - * @param param {string} - name of the query param to set - * @param value {string|number} - value to give the query param - * @returns {string} A copy of the original url with the new or updated query param - */ -export const setUrlParam = (url, param, value) => { - const [rootAndQuery, fragment] = url.split('#'); - const [root, query] = rootAndQuery.split('?'); - const encodedParam = encodeURIComponent(param); - const encodedPair = `${encodedParam}=${encodeURIComponent(value)}`; - - let paramExists = false; - const paramArray = - (query ? query.split('&') : []) - .map(paramPair => { - const [foundParam] = paramPair.split('='); - if (foundParam === encodedParam) { - paramExists = true; - return encodedPair; - } - return paramPair; - }); - - if (paramExists === false) { - paramArray.push(encodedPair); - } - - const writableFragment = fragment ? `#${fragment}` : ''; - return `${root}?${paramArray.join('&')}${writableFragment}`; -}; - -/** - * Remove the query param from the given url by returning a new url string that no longer includes - * the param/value pair. - * - * @param url {string} - url from which the query param will be removed - * @param param {string} - the name of the query param to remove - * @returns {string} A copy of the original url but without the query param - */ -export const removeUrlParam = (url, param) => { - const [rootAndQuery, fragment] = url.split('#'); - const [root, query] = rootAndQuery.split('?'); - - if (query === undefined) { - return url; - } - - const encodedParam = encodeURIComponent(param); - const updatedQuery = query - .split('&') - .filter(paramPair => { - const [foundParam] = paramPair.split('='); - return foundParam !== encodedParam; - }) - .join('&'); - - const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : ''; - const writableFragment = fragment ? `#${fragment}` : ''; - return `${root}${writableQuery}${writableFragment}`; -}; - -/** - * Apply the fragment to the given url by returning a new url string that includes - * the fragment. If the given url already contains a fragment, the original fragment - * will be removed. - * - * @param url {string} - url to which the fragment will be applied - * @param fragment {string} - fragment to append - */ -export const setUrlFragment = (url, fragment) => { - const [rootUrl] = url.split('#'); - const encodedFragment = encodeURIComponent(fragment.replace(/^#/, '')); - return `${rootUrl}#${encodedFragment}`; -}; - export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // Identify following special clicks diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 9850f7ce782..61f53a632b8 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,3 +1,5 @@ +import { windowLocation } from './common_utils'; + // Returns an array containing the value(s) of the // of the key passed as an argument export function getParameterValues(sParam) { @@ -42,22 +44,35 @@ export function mergeUrlParams(params, url) { return `${urlparts[1]}?${query}${urlparts[3]}`; } -export function removeParamQueryString(url, param) { - const decodedUrl = decodeURIComponent(url); - const urlVariables = decodedUrl.split('&'); - - return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&'); -} - -export function removeParams(params, source = window.location.href) { - const url = document.createElement('a'); - url.href = source; +/** + * Removes specified query params from the url by returning a new url string that no longer + * includes the param/value pair. If no url is provided, `window.location.href` is used as + * the default value. + * + * @param {string[]} params - the query param names to remove + * @param {string} [url=windowLocation().href] - url from which the query param will be removed + * @returns {string} A copy of the original url but without the query param + */ +export function removeParams(params, url = windowLocation().href) { + const [rootAndQuery, fragment] = url.split('#'); + const [root, query] = rootAndQuery.split('?'); + + if (query === undefined) { + return url; + } - params.forEach(param => { - url.search = removeParamQueryString(url.search, param); - }); + const encodedParams = params.map(param => encodeURIComponent(param)); + const updatedQuery = query + .split('&') + .filter(paramPair => { + const [foundParam] = paramPair.split('='); + return encodedParams.indexOf(foundParam) < 0; + }) + .join('&'); - return url.href; + const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : ''; + const writableFragment = fragment ? `#${fragment}` : ''; + return `${root}${writableQuery}${writableFragment}`; } export function getLocationHash(url = window.location.href) { @@ -66,6 +81,20 @@ export function getLocationHash(url = window.location.href) { return hashIndex === -1 ? null : url.substring(hashIndex + 1); } +/** + * Apply the fragment to the given url by returning a new url string that includes + * the fragment. If the given url already contains a fragment, the original fragment + * will be removed. + * + * @param {string} url - url to which the fragment will be applied + * @param {string} fragment - fragment to append + */ +export const setUrlFragment = (url, fragment) => { + const [rootUrl] = url.split('#'); + const encodedFragment = encodeURIComponent(fragment.replace(/^#/, '')); + return `${rootUrl}#${encodedFragment}`; +}; + export function visitUrl(url, external = false) { if (external) { // Simulate `target="blank" rel="noopener noreferrer"` diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index 0c6ccd6e495..191221a48cd 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { setUrlParam, removeUrlParam } from '~/lib/utils/common_utils'; +import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; /** * OAuth-based login buttons have a separate "remember me" checkbox. @@ -25,9 +25,9 @@ export default class OAuthRememberMe { const href = $(element).attr('href'); if (rememberMe) { - $(element).attr('href', setUrlParam(href, 'remember_me', 1)); + $(element).attr('href', mergeUrlParams({ remember_me: 1 }, href)); } else { - $(element).attr('href', removeUrlParam(href, 'remember_me')); + $(element).attr('href', removeParams(['remember_me'], href)); } }); } diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js index 82ac59224df..71b7ca8ec31 100644 --- a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js +++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js @@ -1,4 +1,4 @@ -import { setUrlFragment, setUrlParam } from '../../../lib/utils/common_utils'; +import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility'; /** * Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and @@ -22,7 +22,7 @@ export default function preserveUrlFragment(fragment) { // query param will be available in the omniauth callback upon successful authentication const anchors = document.querySelectorAll('#signin-container a.oauth-login'); Array.prototype.forEach.call(anchors, (anchor) => { - const newHref = setUrlParam(anchor.getAttribute('href'), 'redirect_fragment', normalFragment); + const newHref = mergeUrlParams({ redirect_fragment: normalFragment }, anchor.getAttribute('href')); anchor.setAttribute('href', newHref); }); } diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 3a25be766cb..f320f232687 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -65,67 +65,6 @@ describe('common_utils', () => { }); }); - describe('setUrlParam', () => { - it('should append param when url has no other params', () => { - const url = commonUtils.setUrlParam('/feature/home', 'newParam', 'yes'); - expect(url).toBe('/feature/home?newParam=yes'); - }); - - it('should append param when url has other params', () => { - const url = commonUtils.setUrlParam('/feature/home?showAll=true', 'newParam', 'yes'); - expect(url).toBe('/feature/home?showAll=true&newParam=yes'); - }); - - it('should replace param when url contains the param', () => { - const url = commonUtils.setUrlParam('/feature/home?showAll=true&limit=5', 'limit', '100'); - expect(url).toBe('/feature/home?showAll=true&limit=100'); - }); - - it('should update param and preserve fragment', () => { - const url = commonUtils.setUrlParam('/home?q=no&limit=5&showAll=true#H1', 'limit', '100'); - expect(url).toBe('/home?q=no&limit=100&showAll=true#H1'); - }); - }); - - describe('removeUrlParam', () => { - it('should remove param when url has no other params', () => { - const url = commonUtils.removeUrlParam('/feature/home?size=5', 'size'); - expect(url).toBe('/feature/home'); - }); - - it('should remove param when url has other params', () => { - const url = commonUtils.removeUrlParam('/feature/home?q=1&size=5&f=html', 'size'); - expect(url).toBe('/feature/home?q=1&f=html'); - }); - - it('should remove param and preserve fragment', () => { - const url = commonUtils.removeUrlParam('/feature/home?size=5#H2', 'size'); - expect(url).toBe('/feature/home#H2'); - }); - - it('should not modify url if param does not exist', () => { - const url = commonUtils.removeUrlParam('/feature/home?q=1&size=5&f=html', 'locale'); - expect(url).toBe('/feature/home?q=1&size=5&f=html'); - }); - }); - - describe('setUrlFragment', () => { - it('should set fragment when url has no fragment', () => { - const url = commonUtils.setUrlFragment('/home/feature', 'usage'); - expect(url).toBe('/home/feature#usage'); - }); - - it('should set fragment when url has existing fragment', () => { - const url = commonUtils.setUrlFragment('/home/feature#overview', 'usage'); - expect(url).toBe('/home/feature#usage'); - }); - - it('should set fragment when given fragment includes #', () => { - const url = commonUtils.setUrlFragment('/home/feature#overview', '#install'); - expect(url).toBe('/home/feature#install'); - }); - }); - describe('handleLocationHash', () => { beforeEach(() => { spyOn(window.document, 'getElementById').and.callThrough(); diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js index e4df8441793..fe787baf08e 100644 --- a/spec/javascripts/lib/utils/url_utility_spec.js +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -1,4 +1,4 @@ -import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility'; +import UrlUtility, * as urlUtils from '~/lib/utils/url_utility'; describe('URL utility', () => { describe('webIDEUrl', () => { @@ -8,7 +8,7 @@ describe('URL utility', () => { describe('without relative_url_root', () => { it('returns IDE path with route', () => { - expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', ); }); @@ -20,7 +20,7 @@ describe('URL utility', () => { }); it('returns IDE path with route', () => { - expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + expect(urlUtils.webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', ); }); @@ -29,23 +29,83 @@ describe('URL utility', () => { describe('mergeUrlParams', () => { it('adds w', () => { - expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); - expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); }); it('updates w', () => { - expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); }); it('adds multiple params', () => { - expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); + expect(urlUtils.mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); }); it('adds and updates encoded params', () => { - expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + }); + }); + + describe('removeParams', () => { + describe('when url is passed', () => { + it('removes query param with encoded ampersand', () => { + const url = urlUtils.removeParams(['filter'], '/mail?filter=n%3Djoe%26l%3Dhome'); + + expect(url).toBe('/mail'); + }); + + it('should remove param when url has no other params', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?size=5'); + expect(url).toBe('/feature/home'); + }); + + it('should remove param when url has other params', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?q=1&size=5&f=html'); + expect(url).toBe('/feature/home?q=1&f=html'); + }); + + it('should remove param and preserve fragment', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?size=5#H2'); + expect(url).toBe('/feature/home#H2'); + }); + + it('should remove multiple params', () => { + const url = urlUtils.removeParams(['z', 'a'], '/home?z=11111&l=en_US&a=true#H2'); + expect(url).toBe('/home?l=en_US#H2'); + }); + }); + + describe('when no url is passed', () => { + it('should remove params from window.location.href', () => { + spyOnDependency(UrlUtility, 'windowLocation').and.callFake(() => { + const anchor = document.createElement('a'); + anchor.href = 'https://mysite.com/?zip=11111&locale=en_US&ads=false#privacy'; + return anchor; + }); + + const url = urlUtils.removeParams(['locale']); + expect(url).toBe('https://mysite.com/?zip=11111&ads=false#privacy'); + }); + }); + }); + + describe('setUrlFragment', () => { + it('should set fragment when url has no fragment', () => { + const url = urlUtils.setUrlFragment('/home/feature', 'usage'); + expect(url).toBe('/home/feature#usage'); + }); + + it('should set fragment when url has existing fragment', () => { + const url = urlUtils.setUrlFragment('/home/feature#overview', 'usage'); + expect(url).toBe('/home/feature#usage'); + }); + + it('should set fragment when given fragment includes #', () => { + const url = urlUtils.setUrlFragment('/home/feature#overview', '#install'); + expect(url).toBe('/home/feature#install'); }); }); }); |