diff options
Diffstat (limited to 'spec/frontend')
-rw-r--r-- | spec/frontend/helpers/dom_shims/get_client_rects.js | 14 | ||||
-rw-r--r-- | spec/frontend/helpers/dom_shims/index.js | 2 | ||||
-rw-r--r-- | spec/frontend/helpers/dom_shims/scroll_by.js | 7 | ||||
-rw-r--r-- | spec/frontend/helpers/dom_shims/size_properties.js | 19 | ||||
-rw-r--r-- | spec/frontend/lib/utils/common_utils_spec.js | 812 | ||||
-rw-r--r-- | spec/frontend/lib/utils/mock_data.js | 8 | ||||
-rw-r--r-- | spec/frontend/monitoring/components/charts/time_series_spec.js | 53 | ||||
-rw-r--r-- | spec/frontend/monitoring/components/dashboard_spec.js | 7 | ||||
-rw-r--r-- | spec/frontend/monitoring/embed/embed_spec.js | 4 | ||||
-rw-r--r-- | spec/frontend/monitoring/mock_data.js | 170 | ||||
-rw-r--r-- | spec/frontend/monitoring/store/actions_spec.js | 48 | ||||
-rw-r--r-- | spec/frontend/monitoring/store/getters_spec.js | 36 | ||||
-rw-r--r-- | spec/frontend/monitoring/store/mutations_spec.js | 39 | ||||
-rw-r--r-- | spec/frontend/monitoring/store/utils_spec.js | 174 |
14 files changed, 1166 insertions, 227 deletions
diff --git a/spec/frontend/helpers/dom_shims/get_client_rects.js b/spec/frontend/helpers/dom_shims/get_client_rects.js index d740c1bf154..7ba60dd7936 100644 --- a/spec/frontend/helpers/dom_shims/get_client_rects.js +++ b/spec/frontend/helpers/dom_shims/get_client_rects.js @@ -8,14 +8,16 @@ function hasHiddenStyle(node) { return false; } -function createDefaultClientRect() { +function createDefaultClientRect(node) { + const { outerWidth: width, outerHeight: height } = node; + return { - bottom: 0, - height: 0, + bottom: height, + height, left: 0, - right: 0, + right: width, top: 0, - width: 0, + width, x: 0, y: 0, }; @@ -46,5 +48,5 @@ window.Element.prototype.getClientRects = function getClientRects() { return []; } - return [createDefaultClientRect()]; + return [createDefaultClientRect(node)]; }; diff --git a/spec/frontend/helpers/dom_shims/index.js b/spec/frontend/helpers/dom_shims/index.js index 63850b62ff7..1b73f0e2ef5 100644 --- a/spec/frontend/helpers/dom_shims/index.js +++ b/spec/frontend/helpers/dom_shims/index.js @@ -2,3 +2,5 @@ import './element_scroll_into_view'; import './get_client_rects'; import './inner_text'; import './window_scroll_to'; +import './scroll_by'; +import './size_properties'; diff --git a/spec/frontend/helpers/dom_shims/scroll_by.js b/spec/frontend/helpers/dom_shims/scroll_by.js new file mode 100644 index 00000000000..90387e51765 --- /dev/null +++ b/spec/frontend/helpers/dom_shims/scroll_by.js @@ -0,0 +1,7 @@ +window.scrollX = 0; +window.scrollY = 0; + +window.scrollBy = (x, y) => { + window.scrollX += x; + window.scrollY += y; +}; diff --git a/spec/frontend/helpers/dom_shims/size_properties.js b/spec/frontend/helpers/dom_shims/size_properties.js new file mode 100644 index 00000000000..a2d5940bd1e --- /dev/null +++ b/spec/frontend/helpers/dom_shims/size_properties.js @@ -0,0 +1,19 @@ +const convertFromStyle = style => { + if (style.match(/[0-9](px|rem)/g)) { + return Number(style.replace(/[^0-9]/g, '')); + } + + return 0; +}; + +Object.defineProperty(global.HTMLElement.prototype, 'offsetWidth', { + get() { + return convertFromStyle(this.style.width || '0px'); + }, +}); + +Object.defineProperty(global.HTMLElement.prototype, 'offsetHeight', { + get() { + return convertFromStyle(this.style.height || '0px'); + }, +}); diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js new file mode 100644 index 00000000000..d0d45b153af --- /dev/null +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -0,0 +1,812 @@ +import * as commonUtils from '~/lib/utils/common_utils'; + +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(() => { + jest.spyOn(window.document, 'getElementById'); + }); + + afterEach(() => { + window.history.pushState({}, null, ''); + }); + + 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', () => { + jest.spyOn(window, 'scrollBy'); + 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(); + jest.advanceTimersByTime(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', () => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => {}); + + commonUtils.historyPushState('newpath?page=2'); + + expect(window.history.pushState).toHaveBeenCalled(); + expect(window.history.pushState.mock.calls[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 = jest.fn(); + 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', () => { + const testContext = {}; + beforeEach(() => { + testContext.CLRFHeaders = + 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE'; + jest.spyOn(String.prototype, 'split'); + testContext.normalizeCRLFHeaders = commonUtils.normalizeCRLFHeaders(testContext.CLRFHeaders); + }); + + it('should split by newline', () => { + expect(String.prototype.split).toHaveBeenCalledWith('\n'); + }); + + it('should split by colon+space for each header', () => { + expect(String.prototype.split.mock.calls.filter(args => args[0] === ': ').length).toBe(3); + }); + + it('should return a normalized headers object', () => { + expect(testContext.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('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 + jest.spyOn(window, 'setTimeout').mockImplementation(cb => setImmediate(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.mock.calls.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.mock.calls.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('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('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( + expect.objectContaining({ + title: searchSpace.title, + }), + ); + + // String `1` is found in both `iid` and `reference` props so return both + expect(commonUtils.searchBy('1', searchSpace)).toEqual( + expect.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( + expect.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/frontend/lib/utils/mock_data.js b/spec/frontend/lib/utils/mock_data.js new file mode 100644 index 00000000000..c466b0cd1ed --- /dev/null +++ b/spec/frontend/lib/utils/mock_data.js @@ -0,0 +1,8 @@ +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,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGtElEQVRYR8WXf3CT9R3H35/nSdIQIktrCf0RStI0FYRjVBAccxTq5MDBKUoz4ZyjbPO87q4yBsPDMdExTjlvIsdQexyI0oMBeuKhdjsNHhwcMgpjIlublLIm/UlJKZSSJs/z/e6+T5v0CQ22wB/7/pPck8/383l9fj6fEOec8H88NAjAS1LwknsFSVLU8WXd1rtm85LUeKnwGQKzjj3s33azvsEAAEIlnn8ByHL4/Pa7BgAQLCm8QOBOh88vDQkQeMxjMkcQEYKqYsyJWWPhgs/80TsFafzROJtkNIXFfYI0pfXqPeennjqlxPUNikBoTuEmEF+lCRBV3G0aQiWFrwH8d30AWJubGdiEfZzdGqDEEwbICnADQGGHry7zTr0X94IlnnMACggwAWh0+PxOvb5EBGqmTTNkj7ySxWS62C+g5Usm1Zn95YXG24UQ+r5n75Li6Ux4LBkyc7/4t5YSLSr6Lgg9UvBLcKocMEYKON/gGB3YoA/bcGFCczzLQdieLE9bHL66FakBSjzCU0cSAHDa4at7aLhG9XLBEk8zAVnxZxyIEhBy+PwFgwAafpxvNzK5NZUhrX28JA07Cl6SmtvcOUwm4ZAouHj7ad+jMrN1dqb3iG7oS4EYPh2etQS+XiesC8TQ3ZD3yZJsHuUPgbMcI+ej5v3ncv5PasNlk1p7JJnzJL+I0/O5h+u0VCdqIDi78AQRHuirft3hYJzQPvawPydVdPI+/OnTnNNKBjYVXHRa8rFFGeb4w1he0wZ7d/84IXTEhxzxUsgitB2LPFGwvgGUfLSeZUpEXqEqrIdz0nr4iHOUfeOccb/tNMtutzWHPeWcJc0aMxm5lkxYDGloj1zB+Sv/RXXTSXzaeBwSY3j+bHNv2bdtMYCbpHtRkNFd36xFQN3tXkZhvgP1fdPi5kMEXL4oIXKVAA58M8aCVQs84BYLXi5aDq+zGJTqYr+i4PV2vHxmJ/7WUoOn2i/jz6yhW7JjrdSV8U4fQFV+I2Q4UIsedMCSSlcsgp72WtnSajOhzDsBNtsYfFD8e+Rbs4fdIG98uw9vnj+AX7FWvk4NHZOXXphF/INx2SpJIU2L8L4GDAoMwlP9kWSg6awcKVs83tyUnY5Dj75+W8bjutae3o5d9X/HTiWAuUtOS6RUOR8Hp48TxjgU/AMSeKJ1Ej/tMWXG1sxwGt98sBxe5+xhe64XVLiK2Z9XwNgdRLXyzQsC4ENwelIHAFxDBOdh1qdCdNLCoon8RnY+HZ6/+TtzPhTZweAxlJ94C5VqoI2U3a7rACzJjQqgBd24CGscos1kxPQZ38fqSU/jhQkDvN9lrKG7FeUnNuPVKcvwYOb4hGgvi2HSx8vwRKyJkVLl+hk43gdBAcfADBD1cA4RXIdZ1EN1Zjqem+DGoUc2oigjMUlvaV8YL/1qPVpuhOG+JwdH5m1Okn3m6Eacaz3V2jeI9uTbVYY6AKOSKw8MX0MBg2lXjh3r3Hk4s7ASdrMtSWxnoBpZIzIwP3e69lxv3Gay4q/F6zDJ5kq6s6amEnsafJ0Db8P9JKkx1w5wPJuY36IToojgNMzb8rLwmsuB2kW7YDWMSCgTg+YXx9+AQZKxdUaFZiju+a2Mi8uvnH0f2/2f9g4AVE4z4LlTilrlehag9xIpEam4jO4DXfdaV97nwtH5byW137VYD5Yc2YAz4YAGIYx2RLq0z1Sex8l//fUWfBI83jh4Kd1PEuAwqVGjWEwSS+nJJmt0sWu86d0frMQCR/LbWQ8hDAxlXMgUV69Q67ubv0q5FUNAlHKmVLnXE/gfREpUiaQHqAizXbO0UN98BMTSo39Cw7UW7E2Rc728qJGHP68ASbQyNYCQTkAUzCSwQ+CwvSjnsQPGLOnI/C0YO3Lwxq5yhhtqb1KNpGqT1TXvigJU0jh33xpAf7NymoGNDJ9sJtPkYuNkqTh7KnY8vGaoeZPy93+GA1joe4kzzv/SVLqvYngA/dFgVfnlb8tjtm6Ux+I39y/Gqone24IQM+GxL15UO3q7WrhsnhJatCs8PAC9md3OrPK0goaDyEj7uXsuXi0qg4HkIUGE52XHNqmXIl0RGOiHoUV7xb+v5K14SC39At79Ximdhc8ekjImuiyjsXryUszLnY40yThIhSi4bbUHsbfBJ6ZKE5dpQdz4HQOgf2a8tLvklY+M6cuvSnJummxSZ46+X+7biMzaRnSu84IauNYsE5HCOX+HDCPWi7DrKW8/BTcVZ2UN8Me57kc5448TaCYR5XJwC0BtHMwPjs/SgAP1pfuCqSL8Pxhr/wunLWAOAAAAAElFTkSuQmCC'; diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 4dd376faac0..e9322d6b5a9 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -12,6 +12,7 @@ import { deploymentData, metricsDashboardPayload, mockedQueryResultPayload, + metricsDashboardViewModel, mockProjectDir, mockHost, } from '../../mock_data'; @@ -65,7 +66,7 @@ describe('Time series component', () => { ); // Pick the second panel group and the first panel in it - [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; + [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[0].panels; }); describe('general functions', () => { @@ -188,7 +189,7 @@ describe('Time series component', () => { }); it('formats tooltip content', () => { - const name = 'Pod average'; + const name = 'Total'; const value = '5.556'; const dataIndex = 0; const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); @@ -439,7 +440,7 @@ describe('Time series component', () => { it('constructs a label for the chart y-axis', () => { const { yAxis } = getChartOptions(); - expect(yAxis[0].name).toBe('Memory Used per Pod'); + expect(yAxis[0].name).toBe('Total Memory Used'); }); }); }); @@ -535,48 +536,24 @@ describe('Time series component', () => { }); describe('with multiple time series', () => { - const mockedResultMultipleSeries = []; - const [, , panelData] = metricsDashboardPayload.panel_groups[1].panels; - - for (let i = 0; i < panelData.metrics.length; i += 1) { - mockedResultMultipleSeries.push(cloneDeep(mockedQueryResultPayload)); - mockedResultMultipleSeries[ - i - ].metricId = `${panelData.metrics[i].metric_id}_${panelData.metrics[i].id}`; - } - - beforeEach(() => { - setTestTimeout(1000); - - store = createStore(); - - store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, - metricsDashboardPayload, - ); - - store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - - // Mock data contains the metric_id for a multiple time series panel - for (let i = 0; i < panelData.metrics.length; i += 1) { - store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedResultMultipleSeries[i], - ); - } - - // Pick the second panel group and the second panel in it - [, , mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; - }); - describe('General functions', () => { let timeSeriesChart; beforeEach(done => { - timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); + store = createStore(); + const graphData = cloneDeep(metricsDashboardViewModel.panelGroups[0].panels[3]); + graphData.metrics.forEach(metric => + Object.assign(metric, { result: mockedQueryResultPayload.result }), + ); + + timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart'); timeSeriesChart.vm.$nextTick(done); }); + afterEach(() => { + timeSeriesChart.destroy(); + }); + describe('computed', () => { let chartData; diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index fcf70a1af63..6f05207204e 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -17,12 +17,13 @@ import { setupComponentStore, propsData } from '../init_utils'; import { metricsDashboardPayload, mockedQueryResultPayload, + metricsDashboardViewModel, environmentData, dashboardGitResponse, } from '../mock_data'; const localVue = createLocalVue(); -const expectedPanelCount = 3; +const expectedPanelCount = 4; describe('Dashboard', () => { let store; @@ -366,7 +367,7 @@ describe('Dashboard', () => { it('metrics can be swapped', () => { const firstDraggable = findDraggables().at(0); - const mockMetrics = [...metricsDashboardPayload.panel_groups[1].panels]; + const mockMetrics = [...metricsDashboardViewModel.panelGroups[0].panels]; const firstTitle = mockMetrics[0].title; const secondTitle = mockMetrics[1].title; @@ -376,7 +377,7 @@ describe('Dashboard', () => { firstDraggable.vm.$emit('input', mockMetrics); return wrapper.vm.$nextTick(() => { - const { panels } = wrapper.vm.dashboard.panel_groups[1]; + const { panels } = wrapper.vm.dashboard.panelGroups[0]; expect(panels[1].title).toEqual(firstTitle); expect(panels[0].title).toEqual(secondTitle); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js index 3bb70a02bd9..850092c4a72 100644 --- a/spec/frontend/monitoring/embed/embed_spec.js +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -69,8 +69,8 @@ describe('Embed', () => { describe('metrics are available', () => { beforeEach(() => { - store.state.monitoringDashboard.dashboard.panel_groups = groups; - store.state.monitoringDashboard.dashboard.panel_groups[0].panels = metricsData; + store.state.monitoringDashboard.dashboard.panelGroups = groups; + store.state.monitoringDashboard.dashboard.panelGroups[0].panels = metricsData; metricsWithDataGetter.mockReturnValue(metricsWithData); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index bad3962dd8f..32daf990ad3 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -1,3 +1,5 @@ +import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; + // This import path needs to be relative for now because this mock data is used in // Karma specs too, where the helpers/test_constants alias can not be resolved import { TEST_HOST } from '../helpers/test_constants'; @@ -246,7 +248,7 @@ export const mockedEmptyResult = { }; export const mockedQueryResultPayload = { - metricId: '17_system_metrics_kubernetes_container_memory_average', + metricId: '12_system_metrics_kubernetes_container_memory_total', result: [ { metric: {}, @@ -378,122 +380,28 @@ export const environmentData = [ }, ].concat(extraEnvironmentData); -export const metricsDashboardResponse = { - dashboard: { - dashboard: 'Environment metrics', - priority: 1, - panel_groups: [ - { - group: 'System metrics (Kubernetes)', - priority: 5, - panels: [ - { - title: 'Memory Usage (Total)', - type: 'area-chart', - y_label: 'Total Memory Used', - weight: 4, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_total', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', - label: 'Total', - unit: 'GB', - metric_id: 12, - prometheus_endpoint_path: 'http://test', - }, - ], - }, - { - title: 'Core Usage (Total)', - type: 'area-chart', - y_label: 'Total Cores', - weight: 3, - metrics: [ - { - id: 'system_metrics_kubernetes_container_cores_total', - query_range: - 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)', - label: 'Total', - unit: 'cores', - metric_id: 13, - }, - ], - }, - { - title: 'Memory Usage (Pod average)', - type: 'line-chart', - y_label: 'Memory Used per Pod', - weight: 2, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_average', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 14, - }, - ], - }, - ], - }, - ], - }, - status: 'success', -}; - export const metricsDashboardPayload = { dashboard: 'Environment metrics', + priority: 1, panel_groups: [ { - group: 'Response metrics (NGINX Ingress VTS)', - priority: 10, - panels: [ - { - metrics: [ - { - id: 'response_metrics_nginx_ingress_throughput_status_code', - label: 'Status Code', - metric_id: 1, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', - query_range: - 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)', - unit: 'req / sec', - }, - ], - title: 'Throughput', - type: 'area-chart', - weight: 1, - y_label: 'Requests / Sec', - }, - ], - }, - { group: 'System metrics (Kubernetes)', priority: 5, panels: [ { - title: 'Memory Usage (Pod average)', + title: 'Memory Usage (Total)', type: 'area-chart', - y_label: 'Memory Used per Pod', - weight: 2, + y_label: 'Total Memory Used', + weight: 4, metrics: [ { - id: 'system_metrics_kubernetes_container_memory_average', + id: 'system_metrics_kubernetes_container_memory_total', query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 17, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024', - appearance: { - line: { - width: 2, - }, - }, + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', + label: 'Total', + unit: 'GB', + metric_id: 12, + prometheus_endpoint_path: 'http://test', }, ], }, @@ -514,6 +422,22 @@ export const metricsDashboardPayload = { ], }, { + title: 'Memory Usage (Pod average)', + type: 'line-chart', + y_label: 'Memory Used per Pod', + weight: 2, + metrics: [ + { + id: 'system_metrics_kubernetes_container_memory_average', + query_range: + 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', + label: 'Pod average', + unit: 'MB', + metric_id: 14, + }, + ], + }, + { title: 'memories', type: 'area-chart', y_label: 'memories', @@ -557,9 +481,45 @@ export const metricsDashboardPayload = { }, ], }, + { + group: 'Response metrics (NGINX Ingress VTS)', + priority: 10, + panels: [ + { + metrics: [ + { + id: 'response_metrics_nginx_ingress_throughput_status_code', + label: 'Status Code', + metric_id: 1, + prometheus_endpoint_path: + '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', + query_range: + 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)', + unit: 'req / sec', + }, + ], + title: 'Throughput', + type: 'area-chart', + weight: 1, + y_label: 'Requests / Sec', + }, + ], + }, ], }; +/** + * Mock of response of metrics_dashboard.json + */ +export const metricsDashboardResponse = { + all_dashboards: [], + dashboard: metricsDashboardPayload, + metrics_data: {}, + status: 'success', +}; + +export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload); + const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ default: false, display_name: `Custom Dashboard ${idx}`, diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index 11d3109fcd1..211950facd7 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -3,7 +3,7 @@ import testAction from 'helpers/vuex_action_helper'; import Tracking from '~/tracking'; import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; -import { backOff } from '~/lib/utils/common_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; import createFlash from '~/flash'; import store from '~/monitoring/stores'; @@ -28,11 +28,10 @@ import { deploymentData, environmentData, metricsDashboardResponse, - metricsDashboardPayload, + metricsDashboardViewModel, dashboardGitResponse, } from '../mock_data'; -jest.mock('~/lib/utils/common_utils'); jest.mock('~/flash'); const resetStore = str => { @@ -44,14 +43,17 @@ const resetStore = str => { }; describe('Monitoring store actions', () => { + const { convertObjectPropsToCamelCase } = commonUtils; + let mock; + beforeEach(() => { mock = new MockAdapter(axios); // Mock `backOff` function to remove exponential algorithm delay. jest.useFakeTimers(); - backOff.mockImplementation(callback => { + jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => { const q = new Promise((resolve, reject) => { const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg)); const next = () => callback(next, stop); @@ -69,7 +71,7 @@ describe('Monitoring store actions', () => { resetStore(store); mock.reset(); - backOff.mockReset(); + commonUtils.backOff.mockReset(); createFlash.mockReset(); }); @@ -115,7 +117,6 @@ describe('Monitoring store actions', () => { afterEach(() => { resetStore(store); - jest.restoreAllMocks(); }); it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => { @@ -365,6 +366,7 @@ describe('Monitoring store actions', () => { ); expect(commit).toHaveBeenCalledWith( types.RECEIVE_METRICS_DATA_SUCCESS, + metricsDashboardResponse.dashboard, ); expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params); @@ -443,8 +445,11 @@ describe('Monitoring store actions', () => { .catch(done.fail); }); it('dispatches fetchPrometheusMetric for each panel query', done => { - state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups; - const [metric] = state.dashboard.panel_groups[0].panels[0].metrics; + state.dashboard.panelGroups = convertObjectPropsToCamelCase( + metricsDashboardResponse.dashboard.panel_groups, + ); + + const [metric] = state.dashboard.panelGroups[0].panels[0].metrics; const getters = { metricsWithData: () => [metric.id], }; @@ -473,16 +478,16 @@ describe('Monitoring store actions', () => { }); it('dispatches fetchPrometheusMetric for each panel query, handles an error', done => { - state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups; - const metric = state.dashboard.panel_groups[0].panels[0].metrics[0]; + state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups; + const metric = state.dashboard.panelGroups[0].panels[0].metrics[0]; - // Mock having one out of three metrics failing + // Mock having one out of four metrics failing dispatch.mockRejectedValueOnce(new Error('Error fetching this metric')); dispatch.mockResolvedValue(); fetchPrometheusMetrics({ state, commit, dispatch }, params) .then(() => { - expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch).toHaveBeenCalledTimes(9); // one per metric expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, params, @@ -508,7 +513,12 @@ describe('Monitoring store actions', () => { beforeEach(() => { state = storeState(); [metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics; - [data] = metricsDashboardPayload.panel_groups[0].panels[0].metrics; + metric = convertObjectPropsToCamelCase(metric, { deep: true }); + + data = { + metricId: metric.metricId, + result: [1582065167.353, 5, 1582065599.353], + }; }); it('commits result', done => { @@ -522,13 +532,13 @@ describe('Monitoring store actions', () => { { type: types.REQUEST_METRIC_RESULT, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, }, }, { type: types.RECEIVE_METRIC_RESULT_SUCCESS, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, result: data.result, }, }, @@ -556,13 +566,13 @@ describe('Monitoring store actions', () => { { type: types.REQUEST_METRIC_RESULT, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, }, }, { type: types.RECEIVE_METRIC_RESULT_SUCCESS, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, result: data.result, }, }, @@ -592,13 +602,13 @@ describe('Monitoring store actions', () => { { type: types.REQUEST_METRIC_RESULT, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, }, }, { type: types.RECEIVE_METRIC_RESULT_FAILURE, payload: { - metricId: metric.metric_id, + metricId: metric.metricId, error, }, }, diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index 263050b462f..64601e892ad 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -32,7 +32,7 @@ describe('Monitoring store Getters', () => { it('when dashboard has no panel groups, returns empty', () => { setupState({ dashboard: { - panel_groups: [], + panelGroups: [], }, }); @@ -43,10 +43,10 @@ describe('Monitoring store Getters', () => { let groups; beforeEach(() => { setupState({ - dashboard: { panel_groups: [] }, + dashboard: { panelGroups: [] }, }); mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); - groups = state.dashboard.panel_groups; + groups = state.dashboard.panelGroups; }); it('no loaded metric returns empty', () => { @@ -84,8 +84,8 @@ describe('Monitoring store Getters', () => { expect(getMetricStates()).toEqual([metricStates.OK]); // Filtered by groups - expect(getMetricStates(state.dashboard.panel_groups[0].key)).toEqual([]); - expect(getMetricStates(state.dashboard.panel_groups[1].key)).toEqual([metricStates.OK]); + expect(getMetricStates(state.dashboard.panelGroups[0].key)).toEqual([metricStates.OK]); + expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([]); }); it('on multiple metrics errors', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); @@ -94,10 +94,10 @@ describe('Monitoring store Getters', () => { metricId: groups[0].panels[0].metrics[0].metricId, }); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[0].metrics[0].metricId, + metricId: groups[0].panels[0].metrics[0].metricId, }); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[1].metrics[0].metricId, + metricId: groups[1].panels[0].metrics[0].metricId, }); // Entire dashboard fails @@ -113,18 +113,18 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload); // An error in 2 groups mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, + metricId: groups[0].panels[1].metrics[0].metricId, }); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[1].metrics[0].metricId, + metricId: groups[1].panels[0].metrics[0].metricId, }); expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]); - expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]); - expect(getMetricStates(groups[1].key)).toEqual([ + expect(getMetricStates(groups[0].key)).toEqual([ metricStates.OK, metricStates.UNKNOWN_ERROR, ]); + expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]); }); }); }); @@ -154,7 +154,7 @@ describe('Monitoring store Getters', () => { it('when dashboard has no panel groups, returns empty', () => { setupState({ dashboard: { - panel_groups: [], + panelGroups: [], }, }); @@ -164,7 +164,7 @@ describe('Monitoring store Getters', () => { describe('when the dashboard is set', () => { beforeEach(() => { setupState({ - dashboard: { panel_groups: [] }, + dashboard: { panelGroups: [] }, }); }); @@ -204,14 +204,14 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload); mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal); - // First group has no metrics - expect(metricsWithData(state.dashboard.panel_groups[0].key)).toEqual([]); - - // Second group has metrics - expect(metricsWithData(state.dashboard.panel_groups[1].key)).toEqual([ + // First group has metrics + expect(metricsWithData(state.dashboard.panelGroups[0].key)).toEqual([ mockedQueryResultPayload.metricId, mockedQueryResultPayloadCoresTotal.metricId, ]); + + // Second group has no metrics + expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([]); }); }); }); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 3fb7b84fae5..76efc68788d 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -4,12 +4,8 @@ import mutations from '~/monitoring/stores/mutations'; import * as types from '~/monitoring/stores/mutation_types'; import state from '~/monitoring/stores/state'; import { metricStates } from '~/monitoring/constants'; -import { - metricsDashboardPayload, - deploymentData, - metricsDashboardResponse, - dashboardGitResponse, -} from '../mock_data'; + +import { metricsDashboardPayload, deploymentData, dashboardGitResponse } from '../mock_data'; describe('Monitoring mutations', () => { let stateCopy; @@ -17,27 +13,29 @@ describe('Monitoring mutations', () => { beforeEach(() => { stateCopy = state(); }); + describe('RECEIVE_METRICS_DATA_SUCCESS', () => { let payload; - const getGroups = () => stateCopy.dashboard.panel_groups; + const getGroups = () => stateCopy.dashboard.panelGroups; beforeEach(() => { - stateCopy.dashboard.panel_groups = []; + stateCopy.dashboard.panelGroups = []; payload = metricsDashboardPayload; }); it('adds a key to the group', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); const groups = getGroups(); - expect(groups[0].key).toBe('response-metrics-nginx-ingress-vts-0'); - expect(groups[1].key).toBe('system-metrics-kubernetes-1'); + expect(groups[0].key).toBe('system-metrics-kubernetes-0'); + expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1'); }); it('normalizes values', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); const expectedLabel = 'Pod average'; - const { label, query_range } = getGroups()[1].panels[0].metrics[0]; + + const { label, queryRange } = getGroups()[0].panels[2].metrics[0]; expect(label).toEqual(expectedLabel); - expect(query_range.length).toBeGreaterThan(0); + expect(queryRange.length).toBeGreaterThan(0); }); it('contains two groups, with panels with a metric each', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); @@ -47,13 +45,14 @@ describe('Monitoring mutations', () => { expect(groups).toBeDefined(); expect(groups).toHaveLength(2); - expect(groups[0].panels).toHaveLength(1); + expect(groups[0].panels).toHaveLength(4); expect(groups[0].panels[0].metrics).toHaveLength(1); + expect(groups[0].panels[1].metrics).toHaveLength(1); + expect(groups[0].panels[2].metrics).toHaveLength(1); + expect(groups[0].panels[3].metrics).toHaveLength(5); - expect(groups[1].panels).toHaveLength(3); + expect(groups[1].panels).toHaveLength(1); expect(groups[1].panels[0].metrics).toHaveLength(1); - expect(groups[1].panels[1].metrics).toHaveLength(1); - expect(groups[1].panels[2].metrics).toHaveLength(5); }); it('assigns metrics a metric id', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); @@ -61,10 +60,10 @@ describe('Monitoring mutations', () => { const groups = getGroups(); expect(groups[0].panels[0].metrics[0].metricId).toEqual( - '1_response_metrics_nginx_ingress_throughput_status_code', + '12_system_metrics_kubernetes_container_memory_total', ); expect(groups[1].panels[0].metrics[0].metricId).toEqual( - '17_system_metrics_kubernetes_container_memory_average', + '1_response_metrics_nginx_ingress_throughput_status_code', ); }); }); @@ -130,8 +129,8 @@ describe('Monitoring mutations', () => { values: [[0, 1], [1, 1], [1, 3]], }, ]; - const { dashboard } = metricsDashboardResponse; - const getMetric = () => stateCopy.dashboard.panel_groups[0].panels[0].metrics[0]; + const dashboard = metricsDashboardPayload; + const getMetric = () => stateCopy.dashboard.panelGroups[0].panels[0].metrics[0]; describe('REQUEST_METRIC_RESULT', () => { beforeEach(() => { diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index d322d45457e..57418e90470 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -1,27 +1,169 @@ import { - normalizeMetric, uniqMetricsId, parseEnvironmentsResponse, removeLeadingSlash, + mapToDashboardViewModel, } from '~/monitoring/stores/utils'; const projectPath = 'gitlab-org/gitlab-test'; -describe('normalizeMetric', () => { - [ - { args: [], expected: 'undefined_undefined' }, - { args: [undefined], expected: 'undefined_undefined' }, - { args: [{ id: 'something' }], expected: 'undefined_something' }, - { args: [{ id: 45 }], expected: 'undefined_45' }, - { args: [{ metric_id: 5 }], expected: '5_undefined' }, - { args: [{ metric_id: 'something' }], expected: 'something_undefined' }, - { - args: [{ metric_id: 5, id: 'system_metrics_kubernetes_container_memory_total' }], - expected: '5_system_metrics_kubernetes_container_memory_total', - }, - ].forEach(({ args, expected }) => { - it(`normalizes metric to "${expected}" with args=${JSON.stringify(args)}`, () => { - expect(normalizeMetric(...args)).toEqual({ metric_id: expected, metricId: expected }); +describe('mapToDashboardViewModel', () => { + it('maps an empty dashboard', () => { + expect(mapToDashboardViewModel({})).toEqual({ + dashboard: '', + panelGroups: [], + }); + }); + + it('maps a simple dashboard', () => { + const response = { + dashboard: 'Dashboard Name', + panel_groups: [ + { + group: 'Group 1', + panels: [ + { + title: 'Title A', + type: 'chart-type', + y_label: 'Y Label A', + metrics: [], + }, + ], + }, + ], + }; + + expect(mapToDashboardViewModel(response)).toEqual({ + dashboard: 'Dashboard Name', + panelGroups: [ + { + group: 'Group 1', + key: 'group-1-0', + panels: [ + { + title: 'Title A', + type: 'chart-type', + y_label: 'Y Label A', + metrics: [], + }, + ], + }, + ], + }); + }); + + describe('panel groups mapping', () => { + it('key', () => { + const response = { + dashboard: 'Dashboard Name', + panel_groups: [ + { + group: 'Group A', + }, + { + group: 'Group B', + }, + { + group: '', + unsupported_property: 'This should be removed', + }, + ], + }; + + expect(mapToDashboardViewModel(response).panelGroups).toEqual([ + { + group: 'Group A', + key: 'group-a-0', + panels: [], + }, + { + group: 'Group B', + key: 'group-b-1', + panels: [], + }, + { + group: '', + key: 'default-2', + panels: [], + }, + ]); + }); + }); + + describe('metrics mapping', () => { + const defaultLabel = 'Panel Label'; + const dashboardWithMetric = (metric, label = defaultLabel) => ({ + panel_groups: [ + { + panels: [ + { + y_label: label, + metrics: [metric], + }, + ], + }, + ], + }); + + const getMappedMetric = dashboard => { + return mapToDashboardViewModel(dashboard).panelGroups[0].panels[0].metrics[0]; + }; + + it('creates a metric', () => { + const dashboard = dashboardWithMetric({}); + + expect(getMappedMetric(dashboard)).toEqual({ + label: expect.any(String), + metricId: expect.any(String), + metric_id: expect.any(String), + }); + }); + + it('creates a metric with a correct ids', () => { + const dashboard = dashboardWithMetric({ + id: 'http_responses', + metric_id: 1, + }); + + expect(getMappedMetric(dashboard)).toMatchObject({ + metricId: '1_http_responses', + metric_id: '1_http_responses', + }); + }); + + it('creates a metric with a default label', () => { + const dashboard = dashboardWithMetric({}); + + expect(getMappedMetric(dashboard)).toMatchObject({ + label: defaultLabel, + }); + }); + + it('creates a metric with an endpoint and query', () => { + const dashboard = dashboardWithMetric({ + prometheus_endpoint_path: 'http://test', + query_range: 'http_responses', + }); + + expect(getMappedMetric(dashboard)).toMatchObject({ + prometheusEndpointPath: 'http://test', + queryRange: 'http_responses', + }); + }); + + it('creates a metric with an ad-hoc property', () => { + // This behavior is deprecated and should be removed + // https://gitlab.com/gitlab-org/gitlab/issues/207198 + + const dashboard = dashboardWithMetric({ + x_label: 'Another label', + unkown_option: 'unkown_data', + }); + + expect(getMappedMetric(dashboard)).toMatchObject({ + x_label: 'Another label', + unkown_option: 'unkown_data', + }); }); }); }); |