diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-27 03:09:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-27 03:09:19 +0300 |
commit | 0a0e82d1440b06650e5fc524168b1f50a8feec68 (patch) | |
tree | c2202560fb250008cf4109e99537b10604faf01b /spec/javascripts/lib | |
parent | f82d5dcab7c3d9a672abc827c92f86887b683a7d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/javascripts/lib')
-rw-r--r-- | spec/javascripts/lib/utils/browser_spec.js | 175 | ||||
-rw-r--r-- | spec/javascripts/lib/utils/common_utils_spec.js | 981 | ||||
-rw-r--r-- | spec/javascripts/lib/utils/mock_data.js | 9 |
3 files changed, 176 insertions, 989 deletions
diff --git a/spec/javascripts/lib/utils/browser_spec.js b/spec/javascripts/lib/utils/browser_spec.js new file mode 100644 index 00000000000..6b1074a3b4f --- /dev/null +++ b/spec/javascripts/lib/utils/browser_spec.js @@ -0,0 +1,175 @@ +/** + * This file should only contain browser specific specs. + * If you need to add or update a spec, please see spec/frontend/lib/utils/*.js + * https://gitlab.com/gitlab-org/gitlab/issues/194242#note_292137135 + * https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment + */ + +import MockAdapter from 'axios-mock-adapter'; +import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; +import axios from '~/lib/utils/axios_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; + +const PIXEL_TOLERANCE = 0.2; + +/** + * Loads a data URL as the src of an + * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image|Image} + * and resolves to that Image once loaded. + * + * @param url + * @returns {Promise} + */ +const urlToImage = url => + new Promise(resolve => { + const img = new Image(); + img.onload = function() { + resolve(img); + }; + img.src = url; + }); + +describe('common_utils browser specific specs', () => { + describe('contentTop', () => { + it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => { + spyOn(breakpointInstance, 'isDesktop').and.returnValue(false); + + setFixtures(` + <div class="diff-file file-title-flex-parent"> + blah blah blah + </div> + <div class="mr-version-controls"> + more blah blah blah + </div> + `); + + expect(commonUtils.contentTop()).toBe(0); + }); + + it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => { + spyOn(breakpointInstance, 'isDesktop').and.returnValue(true); + + setFixtures(` + <div class="diff-file file-title-flex-parent"> + blah blah blah + </div> + <div class="mr-version-controls"> + more blah blah blah + </div> + `); + + expect(commonUtils.contentTop()).toBe(18); + }); + }); + + describe('createOverlayIcon', () => { + it('should return the favicon with the overlay', done => { + commonUtils + .createOverlayIcon(faviconDataUrl, overlayDataUrl) + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); + done(); + }) + .catch(done.fail); + }); + }); + + describe('setFaviconOverlay', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should set page favicon to provided favicon overlay', done => { + commonUtils + .setFaviconOverlay(overlayDataUrl) + .then(() => document.getElementById('favicon').getAttribute('href')) + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); + done(); + }) + .catch(done.fail); + }); + }); + + describe('setCiStatusFavicon', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; + let mock; + + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'null'); + favicon.setAttribute('data-original-href', faviconDataUrl); + document.body.appendChild(favicon); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should reset favicon in case of error', done => { + mock.onGet(BUILD_URL).replyOnce(500); + + commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => { + const favicon = document.getElementById('favicon'); + + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); + done(); + }); + }); + + it('should set page favicon to CI status favicon based on provided status', done => { + mock.onGet(BUILD_URL).reply(200, { + favicon: overlayDataUrl, + }); + + commonUtils + .setCiStatusFavicon(BUILD_URL) + .then(() => document.getElementById('favicon').getAttribute('href')) + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); + done(); + }) + .catch(done.fail); + }); + }); + + describe('isInViewport', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + }); + + afterEach(() => { + document.body.removeChild(el); + }); + + it('returns true when provided `el` is in viewport', () => { + el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`); + document.body.appendChild(el); + + expect(commonUtils.isInViewport(el)).toBe(true); + }); + + it('returns false when provided `el` is not in viewport', () => { + el.setAttribute('style', 'position: absolute; top: -1000px; left: -1000px;'); + document.body.appendChild(el); + + expect(commonUtils.isInViewport(el)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js deleted file mode 100644 index 504d4a3e01a..00000000000 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ /dev/null @@ -1,981 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; -import axios from '~/lib/utils/axios_utils'; -import * as commonUtils from '~/lib/utils/common_utils'; -import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; - -const PIXEL_TOLERANCE = 0.2; - -/** - * Loads a data URL as the src of an - * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image|Image} - * and resolves to that Image once loaded. - * - * @param url - * @returns {Promise} - */ -const urlToImage = url => - new Promise(resolve => { - const img = new Image(); - img.onload = function() { - resolve(img); - }; - img.src = url; - }); - -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('parseUrlPathname', () => { - it('returns an absolute url when given an absolute url', () => { - expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url'); - }); - - it('returns an absolute url when given a relative url', () => { - expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); - }); - }); - - describe('urlParamsToArray', () => { - it('returns empty array for empty querystring', () => { - expect(commonUtils.urlParamsToArray('')).toEqual([]); - }); - - it('should decode params', () => { - expect(commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0]).toBe('label_name[]=test'); - }); - - it('should remove the question mark from the search params', () => { - const paramsArray = commonUtils.urlParamsToArray('?test=thing'); - - expect(paramsArray[0][0]).not.toBe('?'); - }); - }); - - describe('urlParamsToObject', () => { - it('parses path for label with trailing +', () => { - expect(commonUtils.urlParamsToObject('label_name[]=label%2B', {})).toEqual({ - label_name: ['label+'], - }); - }); - - it('parses path for milestone with trailing +', () => { - expect(commonUtils.urlParamsToObject('milestone_title=A%2B', {})).toEqual({ - milestone_title: 'A+', - }); - }); - - it('parses path for search terms with spaces', () => { - expect(commonUtils.urlParamsToObject('search=two+words', {})).toEqual({ - search: 'two words', - }); - }); - }); - - describe('handleLocationHash', () => { - beforeEach(() => { - spyOn(window.document, 'getElementById').and.callThrough(); - jasmine.clock().install(); - }); - - afterEach(() => { - window.history.pushState({}, null, ''); - jasmine.clock().uninstall(); - }); - - function expectGetElementIdToHaveBeenCalledWith(elementId) { - expect(window.document.getElementById).toHaveBeenCalledWith(elementId); - } - - it('decodes hash parameter', () => { - window.history.pushState({}, null, '#random-hash'); - commonUtils.handleLocationHash(); - - expectGetElementIdToHaveBeenCalledWith('random-hash'); - expectGetElementIdToHaveBeenCalledWith('user-content-random-hash'); - }); - - it('decodes cyrillic hash parameter', () => { - window.history.pushState({}, null, '#definição'); - commonUtils.handleLocationHash(); - - expectGetElementIdToHaveBeenCalledWith('definição'); - expectGetElementIdToHaveBeenCalledWith('user-content-definição'); - }); - - 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'); - }); - - it('scrolls element into view', () => { - document.body.innerHTML += ` - <div id="parent"> - <div style="height: 2000px;"></div> - <div id="test" style="height: 2000px;"></div> - </div> - `; - - window.history.pushState({}, null, '#test'); - commonUtils.handleLocationHash(); - - expectGetElementIdToHaveBeenCalledWith('test'); - - expect(window.scrollY).toBe(document.getElementById('test').offsetTop); - - document.getElementById('parent').remove(); - }); - - it('scrolls user content element into view', () => { - document.body.innerHTML += ` - <div id="parent"> - <div style="height: 2000px;"></div> - <div id="user-content-test" style="height: 2000px;"></div> - </div> - `; - - window.history.pushState({}, null, '#test'); - commonUtils.handleLocationHash(); - - expectGetElementIdToHaveBeenCalledWith('test'); - expectGetElementIdToHaveBeenCalledWith('user-content-test'); - - expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop); - - document.getElementById('parent').remove(); - }); - - it('scrolls to element with offset from navbar', () => { - spyOn(window, 'scrollBy').and.callThrough(); - document.body.innerHTML += ` - <div id="parent"> - <div class="navbar-gitlab" style="position: fixed; top: 0; height: 50px;"></div> - <div style="height: 2000px; margin-top: 50px;"></div> - <div id="user-content-test" style="height: 2000px;"></div> - </div> - `; - - window.history.pushState({}, null, '#test'); - commonUtils.handleLocationHash(); - jasmine.clock().tick(1); - - expectGetElementIdToHaveBeenCalledWith('test'); - expectGetElementIdToHaveBeenCalledWith('user-content-test'); - - expect(window.scrollY).toBe(document.getElementById('user-content-test').offsetTop - 50); - expect(window.scrollBy).toHaveBeenCalledWith(0, -50); - - document.getElementById('parent').remove(); - }); - }); - - describe('historyPushState', () => { - afterEach(() => { - window.history.replaceState({}, null, null); - }); - - it('should call pushState with the correct path', () => { - spyOn(window.history, 'pushState'); - - commonUtils.historyPushState('newpath?page=2'); - - expect(window.history.pushState).toHaveBeenCalled(); - expect(window.history.pushState.calls.allArgs()[0][2]).toContain('newpath?page=2'); - }); - }); - - describe('parseQueryStringIntoObject', () => { - it('should return object with query parameters', () => { - expect(commonUtils.parseQueryStringIntoObject('scope=all&page=2')).toEqual({ - scope: 'all', - page: '2', - }); - - expect(commonUtils.parseQueryStringIntoObject('scope=all')).toEqual({ scope: 'all' }); - expect(commonUtils.parseQueryStringIntoObject()).toEqual({}); - }); - }); - - describe('objectToQueryString', () => { - it('returns empty string when `param` is undefined, null or empty string', () => { - expect(commonUtils.objectToQueryString()).toBe(''); - expect(commonUtils.objectToQueryString('')).toBe(''); - }); - - it('returns query string with values of `params`', () => { - const singleQueryParams = { foo: true }; - const multipleQueryParams = { foo: true, bar: true }; - - expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true'); - expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true'); - }); - }); - - describe('buildUrlWithCurrentLocation', () => { - it('should build an url with current location and given parameters', () => { - expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); - expect(commonUtils.buildUrlWithCurrentLocation('?page=2')).toEqual( - `${window.location.pathname}?page=2`, - ); - }); - }); - - describe('debounceByAnimationFrame', () => { - it('debounces a function to allow a maximum of one call per animation frame', done => { - const spy = jasmine.createSpy('spy'); - const debouncedSpy = commonUtils.debounceByAnimationFrame(spy); - window.requestAnimationFrame(() => { - debouncedSpy(); - debouncedSpy(); - window.requestAnimationFrame(() => { - expect(spy).toHaveBeenCalledTimes(1); - done(); - }); - }); - }); - }); - - describe('getParameterByName', () => { - beforeEach(() => { - window.history.pushState({}, null, '?scope=all&p=2'); - }); - - afterEach(() => { - window.history.replaceState({}, null, null); - }); - - 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 = commonUtils.getParameterByName('fakeParameter'); - - expect(value).toBe(null); - }); - - it('should return valid paramentes if URL is provided', () => { - let value = commonUtils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); - - expect(value).toBe('bar'); - - value = commonUtils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); - - expect(value).toBe('canchu'); - }); - }); - - 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 = commonUtils.normalizeHeaders(apiHeaders); - - const WORKHORSE = 'X-SOMETHING-WORKHORSE'; - const NGINX = 'X-SOMETHING-NGINX'; - - expect(normalized[WORKHORSE].workhorse).toBe('ok'); - expect(normalized[NGINX].nginx).toBe('ok'); - }); - }); - - 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); - }); - - it('should split by newline', function() { - expect(String.prototype.split).toHaveBeenCalledWith('\n'); - }); - - 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 return a normalized headers object', function() { - expect(this.normalizeCRLFHeaders).toEqual({ - 'A-HEADER': 'a-value', - 'ANOTHER-HEADER': 'ANOTHER-VALUE', - 'LAST-HEADER': 'last-VALUE', - }); - }); - }); - - 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); - }); - }); - - describe('isMetaClick', () => { - it('should identify meta click on Windows/Linux', () => { - const e = { - metaKey: false, - ctrlKey: true, - which: 1, - }; - - expect(commonUtils.isMetaClick(e)).toBe(true); - }); - - it('should identify meta click on macOS', () => { - const e = { - metaKey: true, - ctrlKey: false, - which: 1, - }; - - expect(commonUtils.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, - }; - - expect(commonUtils.isMetaClick(e)).toBe(true); - }); - }); - - describe('contentTop', () => { - it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => { - spyOn(breakpointInstance, 'isDesktop').and.returnValue(false); - - setFixtures(` - <div class="diff-file file-title-flex-parent"> - blah blah blah - </div> - <div class="mr-version-controls"> - more blah blah blah - </div> - `); - - expect(commonUtils.contentTop()).toBe(0); - }); - - it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => { - spyOn(breakpointInstance, 'isDesktop').and.returnValue(true); - - setFixtures(` - <div class="diff-file file-title-flex-parent"> - blah blah blah - </div> - <div class="mr-version-controls"> - more blah blah blah - </div> - `); - - expect(commonUtils.contentTop()).toBe(18); - }); - }); - - describe('parseBoolean', () => { - const { parseBoolean } = commonUtils; - - it('returns true for "true"', () => { - expect(parseBoolean('true')).toEqual(true); - }); - - it('returns false for "false"', () => { - expect(parseBoolean('false')).toEqual(false); - }); - - it('returns false for "something"', () => { - expect(parseBoolean('something')).toEqual(false); - }); - - it('returns false for null', () => { - expect(parseBoolean(null)).toEqual(false); - }); - - it('is idempotent', () => { - const input = ['true', 'false', 'something', null]; - input.forEach(value => { - const result = parseBoolean(value); - - expect(parseBoolean(result)).toBe(result); - }); - }); - }); - - 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)); - }); - - it('solves the promise from the callback', done => { - const expectedResponseValue = 'Success!'; - commonUtils - .backOff((next, stop) => - new Promise(resolve => { - resolve(expectedResponseValue); - }) - .then(resp => { - stop(resp); - }) - .catch(done.fail), - ) - .then(respBackoff => { - expect(respBackoff).toBe(expectedResponseValue); - done(); - }) - .catch(done.fail); - }); - - 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 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); - } - }) - .catch(done.fail), - ) - .then(respBackoff => { - const timeouts = window.setTimeout.calls.allArgs().map(([, timeout]) => timeout); - - expect(timeouts).toEqual([2000, 4000]); - expect(respBackoff).toBe(expectedResponseValue); - done(); - }) - .catch(done.fail); - }); - - 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('BACKOFF_TIMEOUT'); - done(); - }); - }); - }); - - describe('setFavicon', () => { - beforeEach(() => { - const favicon = document.createElement('link'); - favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('href', 'default/favicon'); - favicon.setAttribute('data-default-href', 'default/favicon'); - document.body.appendChild(favicon); - }); - - afterEach(() => { - document.body.removeChild(document.getElementById('favicon')); - }); - - it('should set page favicon to provided favicon', () => { - const faviconPath = '//custom_favicon'; - commonUtils.setFavicon(faviconPath); - - expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconPath); - }); - }); - - describe('resetFavicon', () => { - beforeEach(() => { - const favicon = document.createElement('link'); - favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('data-original-href', 'default/favicon'); - document.body.appendChild(favicon); - }); - - afterEach(() => { - document.body.removeChild(document.getElementById('favicon')); - }); - - it('should reset page favicon to the default icon', () => { - const favicon = document.getElementById('favicon'); - favicon.setAttribute('href', 'new/favicon'); - commonUtils.resetFavicon(); - - expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); - }); - }); - - describe('createOverlayIcon', () => { - it('should return the favicon with the overlay', done => { - commonUtils - .createOverlayIcon(faviconDataUrl, overlayDataUrl) - .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) - .then(([actual, expected]) => { - expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); - done(); - }) - .catch(done.fail); - }); - }); - - describe('setFaviconOverlay', () => { - beforeEach(() => { - const favicon = document.createElement('link'); - favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('data-original-href', faviconDataUrl); - document.body.appendChild(favicon); - }); - - afterEach(() => { - document.body.removeChild(document.getElementById('favicon')); - }); - - it('should set page favicon to provided favicon overlay', done => { - commonUtils - .setFaviconOverlay(overlayDataUrl) - .then(() => document.getElementById('favicon').getAttribute('href')) - .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) - .then(([actual, expected]) => { - expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); - done(); - }) - .catch(done.fail); - }); - }); - - describe('setCiStatusFavicon', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; - let mock; - - beforeEach(() => { - const favicon = document.createElement('link'); - favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('href', 'null'); - favicon.setAttribute('data-original-href', faviconDataUrl); - document.body.appendChild(favicon); - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - document.body.removeChild(document.getElementById('favicon')); - }); - - it('should reset favicon in case of error', done => { - mock.onGet(BUILD_URL).replyOnce(500); - - commonUtils.setCiStatusFavicon(BUILD_URL).catch(() => { - const favicon = document.getElementById('favicon'); - - expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); - done(); - }); - }); - - it('should set page favicon to CI status favicon based on provided status', done => { - mock.onGet(BUILD_URL).reply(200, { - favicon: overlayDataUrl, - }); - - commonUtils - .setCiStatusFavicon(BUILD_URL) - .then(() => document.getElementById('favicon').getAttribute('href')) - .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) - .then(([actual, expected]) => { - expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); - done(); - }) - .catch(done.fail); - }); - }); - - describe('spriteIcon', () => { - let beforeGon; - - beforeEach(() => { - window.gon = window.gon || {}; - beforeGon = Object.assign({}, window.gon); - window.gon.sprite_icons = 'icons.svg'; - }); - - afterEach(() => { - window.gon = beforeGon; - }); - - it('should return the svg for a linked icon', () => { - expect(commonUtils.spriteIcon('test')).toEqual( - '<svg ><use xlink:href="icons.svg#test" /></svg>', - ); - }); - - it('should set svg className when passed', () => { - expect(commonUtils.spriteIcon('test', 'fa fa-test')).toEqual( - '<svg class="fa fa-test"><use xlink:href="icons.svg#test" /></svg>', - ); - }); - }); - - describe('convertObjectPropsToCamelCase', () => { - it('returns new object with camelCase property names by converting object with snake_case names', () => { - const snakeRegEx = /(_\w)/g; - const mockObj = { - id: 1, - group_name: 'GitLab.org', - absolute_web_url: 'https://gitlab.com/gitlab-org/', - }; - const mappings = { - id: 'id', - groupName: 'group_name', - absoluteWebUrl: 'absolute_web_url', - }; - - const convertedObj = commonUtils.convertObjectPropsToCamelCase(mockObj); - - Object.keys(convertedObj).forEach(prop => { - expect(snakeRegEx.test(prop)).toBeFalsy(); - expect(convertedObj[prop]).toBe(mockObj[mappings[prop]]); - }); - }); - - it('return empty object if method is called with null or undefined', () => { - expect(Object.keys(commonUtils.convertObjectPropsToCamelCase(null)).length).toBe(0); - expect(Object.keys(commonUtils.convertObjectPropsToCamelCase()).length).toBe(0); - expect(Object.keys(commonUtils.convertObjectPropsToCamelCase({})).length).toBe(0); - }); - - it('does not deep-convert by default', () => { - const obj = { - snake_key: { - child_snake_key: 'value', - }, - }; - - expect(commonUtils.convertObjectPropsToCamelCase(obj)).toEqual({ - snakeKey: { - child_snake_key: 'value', - }, - }); - }); - - describe('convertObjectPropsToSnakeCase', () => { - it('converts each object key to snake case', () => { - const obj = { - some: 'some', - 'cool object': 'cool object', - likeThisLongOne: 'likeThisLongOne', - }; - - expect(commonUtils.convertObjectPropsToSnakeCase(obj)).toEqual({ - some: 'some', - cool_object: 'cool object', - like_this_long_one: 'likeThisLongOne', - }); - }); - - it('returns an empty object if there are no keys', () => { - ['', {}, [], null].forEach(badObj => { - expect(commonUtils.convertObjectPropsToSnakeCase(badObj)).toEqual({}); - }); - }); - }); - - describe('with options', () => { - const objWithoutChildren = { - project_name: 'GitLab CE', - group_name: 'GitLab.org', - license_type: 'MIT', - }; - - const objWithChildren = { - project_name: 'GitLab CE', - group_name: 'GitLab.org', - license_type: 'MIT', - tech_stack: { - backend: 'Ruby', - frontend_framework: 'Vue', - database: 'PostgreSQL', - }, - }; - - describe('when options.deep is true', () => { - it('converts object with child objects', () => { - const obj = { - snake_key: { - child_snake_key: 'value', - }, - }; - - expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ - snakeKey: { - childSnakeKey: 'value', - }, - }); - }); - - it('converts array with child objects', () => { - const arr = [ - { - child_snake_key: 'value', - }, - ]; - - expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ - { - childSnakeKey: 'value', - }, - ]); - }); - - it('converts array with child arrays', () => { - const arr = [ - [ - { - child_snake_key: 'value', - }, - ], - ]; - - expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ - [ - { - childSnakeKey: 'value', - }, - ], - ]); - }); - }); - - describe('when options.dropKeys is provided', () => { - it('discards properties mentioned in `dropKeys` array', () => { - expect( - commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, { - dropKeys: ['group_name'], - }), - ).toEqual({ - projectName: 'GitLab CE', - licenseType: 'MIT', - }); - }); - - it('discards properties mentioned in `dropKeys` array when `deep` is true', () => { - expect( - commonUtils.convertObjectPropsToCamelCase(objWithChildren, { - deep: true, - dropKeys: ['group_name', 'database'], - }), - ).toEqual({ - projectName: 'GitLab CE', - licenseType: 'MIT', - techStack: { - backend: 'Ruby', - frontendFramework: 'Vue', - }, - }); - }); - }); - - describe('when options.ignoreKeyNames is provided', () => { - it('leaves properties mentioned in `ignoreKeyNames` array intact', () => { - expect( - commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, { - ignoreKeyNames: ['group_name'], - }), - ).toEqual({ - projectName: 'GitLab CE', - licenseType: 'MIT', - group_name: 'GitLab.org', - }); - }); - - it('leaves properties mentioned in `ignoreKeyNames` array intact when `deep` is true', () => { - expect( - commonUtils.convertObjectPropsToCamelCase(objWithChildren, { - deep: true, - ignoreKeyNames: ['group_name', 'frontend_framework'], - }), - ).toEqual({ - projectName: 'GitLab CE', - group_name: 'GitLab.org', - licenseType: 'MIT', - techStack: { - backend: 'Ruby', - frontend_framework: 'Vue', - database: 'PostgreSQL', - }, - }); - }); - }); - }); - }); - - describe('roundOffFloat', () => { - it('Rounds off decimal places of a float number with provided precision', () => { - expect(commonUtils.roundOffFloat(3.141592, 3)).toBeCloseTo(3.142); - }); - - it('Rounds off a float number to a whole number when provided precision is zero', () => { - expect(commonUtils.roundOffFloat(3.141592, 0)).toBeCloseTo(3); - expect(commonUtils.roundOffFloat(3.5, 0)).toBeCloseTo(4); - }); - - it('Rounds off float number to nearest 0, 10, 100, 1000 and so on when provided precision is below 0', () => { - expect(commonUtils.roundOffFloat(34567.14159, -1)).toBeCloseTo(34570); - expect(commonUtils.roundOffFloat(34567.14159, -2)).toBeCloseTo(34600); - expect(commonUtils.roundOffFloat(34567.14159, -3)).toBeCloseTo(35000); - expect(commonUtils.roundOffFloat(34567.14159, -4)).toBeCloseTo(30000); - expect(commonUtils.roundOffFloat(34567.14159, -5)).toBeCloseTo(0); - }); - }); - - describe('isInViewport', () => { - let el; - - beforeEach(() => { - el = document.createElement('div'); - }); - - afterEach(() => { - document.body.removeChild(el); - }); - - it('returns true when provided `el` is in viewport', () => { - el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`); - document.body.appendChild(el); - - expect(commonUtils.isInViewport(el)).toBe(true); - }); - - it('returns false when provided `el` is not in viewport', () => { - el.setAttribute('style', 'position: absolute; top: -1000px; left: -1000px;'); - document.body.appendChild(el); - - expect(commonUtils.isInViewport(el)).toBe(false); - }); - }); - - describe('searchBy', () => { - const searchSpace = { - iid: 1, - reference: '&1', - title: 'Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate.', - url: '/groups/gitlab-org/-/epics/1', - }; - - it('returns null when `query` or `searchSpace` params are empty/undefined', () => { - expect(commonUtils.searchBy('omnis', null)).toBeNull(); - expect(commonUtils.searchBy('', searchSpace)).toBeNull(); - expect(commonUtils.searchBy()).toBeNull(); - }); - - it('returns object with matching props based on `query` & `searchSpace` params', () => { - // String `omnis` is found only in `title` prop so return just that - expect(commonUtils.searchBy('omnis', searchSpace)).toEqual( - jasmine.objectContaining({ - title: searchSpace.title, - }), - ); - - // String `1` is found in both `iid` and `reference` props so return both - expect(commonUtils.searchBy('1', searchSpace)).toEqual( - jasmine.objectContaining({ - iid: searchSpace.iid, - reference: searchSpace.reference, - }), - ); - - // String `/epics/1` is found in `url` prop so return just that - expect(commonUtils.searchBy('/epics/1', searchSpace)).toEqual( - jasmine.objectContaining({ - url: searchSpace.url, - }), - ); - }); - }); - - describe('isScopedLabel', () => { - it('returns true when `::` is present in title', () => { - expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true); - }); - - it('returns false when `::` is not present', () => { - expect(commonUtils.isScopedLabel({ title: 'foobar' })).toBe(false); - }); - }); - - describe('getDashPath', () => { - it('returns the path following /-/', () => { - expect(commonUtils.getDashPath('/some/-/url-with-dashes-/')).toEqual('url-with-dashes-/'); - }); - - it('returns null when no path follows /-/', () => { - expect(commonUtils.getDashPath('/some/url')).toEqual(null); - }); - }); -}); diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js index c466b0cd1ed..c2f79a32377 100644 --- a/spec/javascripts/lib/utils/mock_data.js +++ b/spec/javascripts/lib/utils/mock_data.js @@ -1,8 +1 @@ -export const faviconDataUrl = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC'; - -export const overlayDataUrl = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; - -export const faviconWithOverlayDataUrl = - 'data:image/png;base64,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'; +export * from '../../../frontend/lib/utils/mock_data.js'; |