diff options
10 files changed, 579 insertions, 74 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index 630e80a7408..3c23b8e472b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -108,8 +108,6 @@ export default { </div> <mr-widget-memory-usage v-if="deployment.metrics_url" - :mr="mr" - :service="service" :metricsUrl="deployment.metrics_url" /> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 395cc9e91fc..486b13e60af 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -5,8 +5,6 @@ import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', props: { - mr: { type: Object, required: true }, - service: { type: Object, required: true }, metricsUrl: { type: String, required: true }, }, data() { @@ -14,6 +12,7 @@ export default { // memoryFrom: 0, // memoryTo: 0, memoryMetrics: [], + deploymentTime: 0, hasMetrics: false, loadFailed: false, loadingMetrics: true, @@ -23,8 +22,22 @@ export default { components: { 'mr-memory-graph': MemoryGraph, }, + computed: { + shouldShowLoading() { + return this.loadingMetrics && !this.hasMetrics && !this.loadFailed; + }, + shouldShowMemoryGraph() { + return !this.loadingMetrics && this.hasMetrics && !this.loadFailed; + }, + shouldShowLoadFailure() { + return !this.loadingMetrics && !this.hasMetrics && this.loadFailed; + }, + shouldShowMetricsUnavailable() { + return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed; + }, + }, methods: { - computeGraphData(metrics) { + computeGraphData(metrics, deploymentTime) { this.loadingMetrics = false; const { memory_values } = metrics; // if (memory_previous.length > 0) { @@ -38,70 +51,73 @@ export default { if (memory_values.length > 0) { this.hasMetrics = true; this.memoryMetrics = memory_values[0].values; + this.deploymentTime = deploymentTime; } }, - }, - mounted() { - this.$props.loadingMetrics = true; - gl.utils.backOff((next, stop) => { - MRWidgetService.fetchMetrics(this.$props.metricsUrl) - .then((res) => { - if (res.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; - if (this.backOffRequestCounter < 3) { - next(); + loadMetrics() { + gl.utils.backOff((next, stop) => { + MRWidgetService.fetchMetrics(this.metricsUrl) + .then((res) => { + if (res.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + /* eslint-disable no-unused-expressions */ + this.backOffRequestCounter < 3 ? next() : stop(res); } else { stop(res); } - } else { - stop(res); + }) + .catch(stop); + }) + .then((res) => { + if (res.status === statusCodes.NO_CONTENT) { + return res; } - }) - .catch(stop); - }) - .then((res) => { - if (res.status === statusCodes.NO_CONTENT) { - return res; - } - return res.json(); - }) - .then((res) => { - this.computeGraphData(res.metrics); - return res; - }) - .catch(() => { - this.$props.loadFailed = true; - }); + return res.json(); + }) + .then((res) => { + this.computeGraphData(res.metrics, res.deployment_time); + return res; + }) + .catch(() => { + this.loadFailed = true; + this.loadingMetrics = false; + }); + }, + }, + mounted() { + this.loadingMetrics = true; + this.loadMetrics(); }, template: ` - <div class="mr-info-list mr-memory-usage"> + <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage"> <div class="legend"></div> <p - v-if="loadingMetrics" - class="usage-info usage-info-loading"> + v-if="shouldShowLoading" + class="usage-info js-usage-info usage-info-loading"> <i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true" />Loading deployment statistics. </p> <p - v-if="!hasMetrics && !loadingMetrics" - class="usage-info usage-info-loading"> - Deployment statistics are not available currently. - </p> - <p - v-if="hasMetrics" - class="usage-info"> + v-if="shouldShowMemoryGraph" + class="usage-info js-usage-info"> Deployment memory usage: </p> <p - v-if="loadFailed" - class="usage-info"> + v-if="shouldShowLoadFailure" + class="usage-info js-usage-info usage-info-failed"> Failed to load deployment statistics. </p> + <p + v-if="shouldShowMetricsUnavailable" + class="usage-info js-usage-info usage-info-unavailable"> + Deployment statistics are not available currently. + </p> <mr-memory-graph - v-if="hasMetrics" + v-if="shouldShowMemoryGraph" :metrics="memoryMetrics" + :deploymentTime="deploymentTime" height="25" width="100" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js index 2a605b24339..643b77e04c7 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.js +++ b/app/assets/javascripts/vue_shared/components/memory_graph.js @@ -2,6 +2,7 @@ export default { name: 'MemoryGraph', props: { metrics: { type: Array, required: true }, + deploymentTime: { type: Number, required: true }, width: { type: String, required: true }, height: { type: String, required: true }, }, @@ -9,27 +10,105 @@ export default { return { pathD: '', pathViewBox: '', - // dotX: '', - // dotY: '', + dotX: '', + dotY: '', }; }, + computed: { + getFormattedMedian() { + const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000); + return `Deployed ${deployedSince}`; + }, + }, + methods: { + /** + * Returns metric value index in metrics array + * with timestamp closest to matching median + */ + getMedianMetricIndex(median, metrics) { + let matchIndex = 0; + let timestampDiff = 0; + let smallestDiff = 0; + + const metricTimestamps = metrics.map(v => v[0]); + + // Find metric timestamp which is closest to deploymentTime + timestampDiff = Math.abs(metricTimestamps[0] - median); + metricTimestamps.forEach((timestamp, index) => { + if (index === 0) { // Skip first element + return; + } + + smallestDiff = Math.abs(timestamp - median); + if (smallestDiff < timestampDiff) { + matchIndex = index; + timestampDiff = smallestDiff; + } + }); + + return matchIndex; + }, + + /** + * Get Graph Plotting values to render Line and Dot + */ + getGraphPlotValues(median, metrics) { + const renderData = metrics.map(v => v[1]); + const medianMetricIndex = this.getMedianMetricIndex(median, metrics); + let cx = 0; + let cy = 0; + + // Find Maximum and Minimum values from `renderData` array + const maxMemory = Math.max.apply(null, renderData); + const minMemory = Math.min.apply(null, renderData); + + // Find difference between extreme ends + const diff = maxMemory - minMemory; + const lineWidth = renderData.length; + + // Iterate over metrics values and perform following + // 1. Find x & y co-ords for deploymentTime's memory value + // 2. Return line path against maxMemory + const linePath = renderData.map((y, x) => { + if (medianMetricIndex === x) { + cx = x; + cy = maxMemory - y; + } + return `${x} ${maxMemory - y}`; + }); + + return { + pathD: linePath, + pathViewBox: { + lineWidth, + diff, + }, + dotX: cx, + dotY: cy, + }; + }, + + /** + * Render Graph based on provided median and metrics values + */ + renderGraph(median, metrics) { + const { pathD, pathViewBox, dotX, dotY } = this.getGraphPlotValues(median, metrics); + + // Set props and update graph on UI. + this.pathD = `M ${pathD}`; + this.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`; + this.dotX = dotX; + this.dotY = dotY; + }, + }, mounted() { - const renderData = this.$props.metrics.map(v => v[1]); - const maxMemory = Math.max.apply(null, renderData); - const minMemory = Math.min.apply(null, renderData); - const diff = maxMemory - minMemory; - // const cx = 0; - // const cy = 0; - const lineWidth = renderData.length; - const linePath = renderData.map((y, x) => `${x} ${maxMemory - y}`); - this.pathD = `M ${linePath}`; - this.pathViewBox = `0 0 ${lineWidth} ${diff}`; + this.renderGraph(this.deploymentTime, this.metrics); }, template: ` <div class="memory-graph-container"> - <svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg"> + <svg class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" xmlns="http://www.w3.org/2000/svg"> <path :d="pathD" :viewBox="pathViewBox" /> - <!--<circle r="0.8" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> --> + <circle r="1.5" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> </svg> </div> `, diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss index 8473f2ef094..81cdf6b59e4 100644 --- a/app/assets/stylesheets/framework/memory_graph.scss +++ b/app/assets/stylesheets/framework/memory_graph.scss @@ -1,16 +1,22 @@ .memory-graph-container { svg { background: $white-light; + cursor: pointer; + + &:hover { + box-shadow: 0 0 4px $gray-darkest inset; + } } path { fill: none; stroke: $blue-500; - stroke-width: 1px; + stroke-width: 2px; } circle { stroke: $blue-700; fill: $blue-700; + stroke-width: 4px; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f4488ccd8fe..97019b19667 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -182,8 +182,7 @@ } &.mr-memory-usage { - margin-top: 10px; - margin-bottom: 10px; + margin: 5px 0 10px 25px; } } @@ -511,7 +510,12 @@ .mr-info-list.mr-memory-usage { .legend { - height: 75%; + height: 65%; + top: 0; + + @media (max-width: $screen-xs-max) { + height: 20px; + } } p { @@ -731,13 +735,15 @@ } .mr-memory-usage { - p.usage-info-loading { - margin-bottom: 6px; + p.usage-info-loading, + p.usage-info-unavailable, + p.usage-info-failed { + margin-bottom: 5px; + } - .usage-info-load-spinner { - margin-right: 10px; - font-size: 16px; - } + p.usage-info-loading .usage-info-load-spinner { + margin-right: 10px; + font-size: 16px; } @media (max-width: $screen-md-min) { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 44c7eb86855..207fbad7856 100755 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -410,10 +410,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController metrics_url = if can?(current_user, :read_environment, environment) && environment.has_metrics? - metrics_namespace_project_environment_path(environment.project.namespace, - environment.project, - environment, - deployment) + metrics_namespace_project_environment_deployment_path(environment.project.namespace, + environment.project, + environment, + deployment) end { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index 3d5f71babfb..2f971b39d16 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -9,6 +9,7 @@ const deploymentMockData = [ name: 'review/diplo', url: '/root/acets-review-apps/environments/15', stop_url: '/root/acets-review-apps/environments/15/stop', + metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics', external_url: 'http://diplo.', external_url_formatted: 'diplo.', deployed_at: '2017-03-22T22:44:42.258Z', @@ -156,6 +157,7 @@ describe('MRWidgetDeployment', () => { expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url); expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted); expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at)); + expect(el.querySelector('.js-mr-memory-usage')).toBeDefined(); expect(el.querySelector('button')).toBeDefined(); }); @@ -165,6 +167,7 @@ describe('MRWidgetDeployment', () => { Vue.nextTick(() => { expect(el.querySelectorAll('.ci-widget').length).toEqual(3); + expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(3); done(); }); }); @@ -176,6 +179,7 @@ describe('MRWidgetDeployment', () => { expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0); expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0); expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0); + expect(el.querySelectorAll('.js-mr-memory-usage').length).toEqual(0); expect(el.querySelectorAll('.button').length).toEqual(0); done(); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js new file mode 100644 index 00000000000..da9dff18ada --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -0,0 +1,184 @@ +import Vue from 'vue'; +import memoryUsageComponent from '~/vue_merge_request_widget/components/mr_widget_memory_usage'; +import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; + +const url = '/root/acets-review-apps/environments/15/deployments/1/metrics'; + +const metricsMockData = { + success: true, + metrics: { + memory_values: [ + { + metric: {}, + values: [ + [1493716685, '4.30859375'], + ], + }, + ], + }, + last_update: '2017-05-02T12:34:49.628Z', + deployment_time: 1493718485, +}; + +const createComponent = () => { + const Component = Vue.extend(memoryUsageComponent); + + return new Component({ + el: document.createElement('div'), + propsData: { + metricsUrl: url, + memoryMetrics: [], + deploymentTime: 0, + hasMetrics: false, + loadFailed: false, + loadingMetrics: true, + backOffRequestCounter: 0, + }, + }); +}; + +const messages = { + loadingMetrics: 'Loading deployment statistics.', + hasMetrics: 'Deployment memory usage:', + loadFailed: 'Failed to load deployment statistics.', + metricsUnavailable: 'Deployment statistics are not available currently.', +}; + +describe('MemoryUsage', () => { + let vm; + let el; + + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + describe('props', () => { + it('should have props with defaults', () => { + const { metricsUrl } = memoryUsageComponent.props; + const MetricsUrlTypeClass = metricsUrl.type; + + Vue.nextTick(() => { + expect(new MetricsUrlTypeClass() instanceof String).toBeTruthy(); + expect(metricsUrl.required).toBeTruthy(); + }); + }); + }); + + describe('data', () => { + it('should have default data', () => { + const data = memoryUsageComponent.data(); + + expect(Array.isArray(data.memoryMetrics)).toBeTruthy(); + expect(data.memoryMetrics.length).toBe(0); + + expect(typeof data.deploymentTime).toBe('number'); + expect(data.deploymentTime).toBe(0); + + expect(typeof data.hasMetrics).toBe('boolean'); + expect(data.hasMetrics).toBeFalsy(); + + expect(typeof data.loadFailed).toBe('boolean'); + expect(data.loadFailed).toBeFalsy(); + + expect(typeof data.loadingMetrics).toBe('boolean'); + expect(data.loadingMetrics).toBeTruthy(); + + expect(typeof data.backOffRequestCounter).toBe('number'); + expect(data.backOffRequestCounter).toBe(0); + }); + }); + + describe('methods', () => { + const { metrics, deployment_time } = metricsMockData; + + describe('computeGraphData', () => { + it('should populate sparkline graph', () => { + vm.computeGraphData(metrics, deployment_time); + const { hasMetrics, memoryMetrics, deploymentTime } = vm; + + expect(hasMetrics).toBeTruthy(); + expect(memoryMetrics.length > 0).toBeTruthy(); + expect(deploymentTime).toEqual(deployment_time); + }); + }); + + describe('loadMetrics', () => { + const returnServicePromise = () => new Promise((resolve) => { + resolve({ + json() { + return metricsMockData; + }, + }); + }); + + it('should load metrics data using MRWidgetService', (done) => { + spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true)); + spyOn(vm, 'computeGraphData'); + + vm.loadMetrics(); + setTimeout(() => { + expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url); + expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time); + done(); + }, 333); + }); + }); + }); + + describe('template', () => { + it('should render template elements correctly', () => { + expect(el.classList.contains('mr-memory-usage')).toBeTruthy(); + expect(el.querySelector('.js-usage-info')).toBeDefined(); + }); + + it('should show loading metrics message while metrics are being loaded', (done) => { + vm.loadingMetrics = true; + vm.hasMetrics = false; + vm.loadFailed = false; + + Vue.nextTick(() => { + expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined(); + expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined(); + expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics); + done(); + }); + }); + + it('should show deployment memory usage when metrics are loaded', (done) => { + vm.loadingMetrics = false; + vm.hasMetrics = true; + vm.loadFailed = false; + + Vue.nextTick(() => { + expect(el.querySelector('.memory-graph-container')).toBeDefined(); + expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics); + done(); + }); + }); + + it('should show failure message when metrics loading failed', (done) => { + vm.loadingMetrics = false; + vm.hasMetrics = false; + vm.loadFailed = true; + + Vue.nextTick(() => { + expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined(); + expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed); + done(); + }); + }); + + it('should show metrics unavailable message when metrics loading failed', (done) => { + vm.loadingMetrics = false; + vm.hasMetrics = false; + vm.loadFailed = false; + + Vue.nextTick(() => { + expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined(); + expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/memory_graph_spec.js b/spec/javascripts/vue_shared/components/memory_graph_spec.js new file mode 100644 index 00000000000..d46a3f2328e --- /dev/null +++ b/spec/javascripts/vue_shared/components/memory_graph_spec.js @@ -0,0 +1,143 @@ +import Vue from 'vue'; +import memoryGraphComponent from '~/vue_shared/components/memory_graph'; +import { mockMetrics, mockMedian, mockMedianIndex } from './mock_data'; + +const defaultHeight = '25'; +const defaultWidth = '100'; + +const createComponent = () => { + const Component = Vue.extend(memoryGraphComponent); + + return new Component({ + el: document.createElement('div'), + propsData: { + metrics: [], + deploymentTime: 0, + width: '', + height: '', + pathD: '', + pathViewBox: '', + dotX: '', + dotY: '', + }, + }); +}; + +describe('MemoryGraph', () => { + let vm; + let el; + + beforeEach(() => { + vm = createComponent(); + el = vm.$el; + }); + + describe('props', () => { + it('should have props with defaults', (done) => { + const { metrics, deploymentTime, width, height } = memoryGraphComponent.props; + + Vue.nextTick(() => { + const typeClassMatcher = (propItem, expectedType) => { + const PropItemTypeClass = propItem.type; + expect(new PropItemTypeClass() instanceof expectedType).toBeTruthy(); + expect(propItem.required).toBeTruthy(); + }; + + typeClassMatcher(metrics, Array); + typeClassMatcher(deploymentTime, Number); + typeClassMatcher(width, String); + typeClassMatcher(height, String); + done(); + }); + }); + }); + + describe('data', () => { + it('should have default data', () => { + const data = memoryGraphComponent.data(); + const dataValidator = (dataItem, expectedType, defaultVal) => { + expect(typeof dataItem).toBe(expectedType); + expect(dataItem).toBe(defaultVal); + }; + + dataValidator(data.pathD, 'string', ''); + dataValidator(data.pathViewBox, 'string', ''); + dataValidator(data.dotX, 'string', ''); + dataValidator(data.dotY, 'string', ''); + }); + }); + + describe('computed', () => { + describe('getFormattedMedian', () => { + it('should show human readable median value based on provided median timestamp', () => { + vm.deploymentTime = mockMedian; + const formattedMedian = vm.getFormattedMedian; + expect(formattedMedian.indexOf('Deployed') > -1).toBeTruthy(); + expect(formattedMedian.indexOf('ago') > -1).toBeTruthy(); + }); + }); + }); + + describe('methods', () => { + describe('getMedianMetricIndex', () => { + it('should return index of closest metric timestamp to that of median', () => { + const matchingIndex = vm.getMedianMetricIndex(mockMedian, mockMetrics); + expect(matchingIndex).toBe(mockMedianIndex); + }); + }); + + describe('getGraphPlotValues', () => { + it('should return Object containing values to plot graph', () => { + const plotValues = vm.getGraphPlotValues(mockMedian, mockMetrics); + expect(plotValues.pathD).toBeDefined(); + expect(Array.isArray(plotValues.pathD)).toBeTruthy(); + + expect(plotValues.pathViewBox).toBeDefined(); + expect(typeof plotValues.pathViewBox).toBe('object'); + + expect(plotValues.dotX).toBeDefined(); + expect(typeof plotValues.dotX).toBe('number'); + + expect(plotValues.dotY).toBeDefined(); + expect(typeof plotValues.dotY).toBe('number'); + }); + }); + }); + + describe('template', () => { + it('should render template elements correctly', () => { + expect(el.classList.contains('memory-graph-container')).toBeTruthy(); + expect(el.querySelector('svg')).toBeDefined(); + }); + + it('should render graph when renderGraph is called internally', (done) => { + const { pathD, pathViewBox, dotX, dotY } = vm.getGraphPlotValues(mockMedian, mockMetrics); + vm.height = defaultHeight; + vm.width = defaultWidth; + vm.pathD = `M ${pathD}`; + vm.pathViewBox = `0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`; + vm.dotX = dotX; + vm.dotY = dotY; + + Vue.nextTick(() => { + const svgEl = el.querySelector('svg'); + expect(svgEl).toBeDefined(); + expect(svgEl.getAttribute('height')).toBe(defaultHeight); + expect(svgEl.getAttribute('width')).toBe(defaultWidth); + + const pathEl = el.querySelector('path'); + expect(pathEl).toBeDefined(); + expect(pathEl.getAttribute('d')).toBe(`M ${pathD}`); + expect(pathEl.getAttribute('viewBox')).toBe(`0 0 ${pathViewBox.lineWidth} ${pathViewBox.diff}`); + + const circleEl = el.querySelector('circle'); + expect(circleEl).toBeDefined(); + expect(circleEl.getAttribute('r')).toBe('1.5'); + expect(circleEl.getAttribute('tranform')).toBe('translate(0 -1)'); + expect(circleEl.getAttribute('cx')).toBe(`${dotX}`); + expect(circleEl.getAttribute('cy')).toBe(`${dotY}`); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/mock_data.js b/spec/javascripts/vue_shared/components/mock_data.js new file mode 100644 index 00000000000..0d781bdca74 --- /dev/null +++ b/spec/javascripts/vue_shared/components/mock_data.js @@ -0,0 +1,69 @@ +/* eslint-disable */ + +export const mockMetrics = [ + [1493716685, '4.30859375'], + [1493716745, '4.30859375'], + [1493716805, '4.30859375'], + [1493716865, '4.30859375'], + [1493716925, '4.30859375'], + [1493716985, '4.30859375'], + [1493717045, '4.30859375'], + [1493717105, '4.30859375'], + [1493717165, '4.30859375'], + [1493717225, '4.30859375'], + [1493717285, '4.30859375'], + [1493717345, '4.30859375'], + [1493717405, '4.30859375'], + [1493717465, '4.30859375'], + [1493717525, '4.30859375'], + [1493717585, '4.30859375'], + [1493717645, '4.30859375'], + [1493717705, '4.30859375'], + [1493717765, '4.30859375'], + [1493717825, '4.30859375'], + [1493717885, '4.30859375'], + [1493717945, '4.30859375'], + [1493718005, '4.30859375'], + [1493718065, '4.30859375'], + [1493718125, '4.30859375'], + [1493718185, '4.30859375'], + [1493718245, '4.30859375'], + [1493718305, '4.234375'], + [1493718365, '4.234375'], + [1493718425, '4.234375'], + [1493718485, '4.234375'], + [1493718545, '4.243489583333333'], + [1493718605, '4.2109375'], + [1493718665, '4.2109375'], + [1493718725, '4.2109375'], + [1493718785, '4.26171875'], + [1493718845, '4.26171875'], + [1493718905, '4.26171875'], + [1493718965, '4.26171875'], + [1493719025, '4.26171875'], + [1493719085, '4.26171875'], + [1493719145, '4.26171875'], + [1493719205, '4.26171875'], + [1493719265, '4.26171875'], + [1493719325, '4.26171875'], + [1493719385, '4.26171875'], + [1493719445, '4.26171875'], + [1493719505, '4.26171875'], + [1493719565, '4.26171875'], + [1493719625, '4.26171875'], + [1493719685, '4.26171875'], + [1493719745, '4.26171875'], + [1493719805, '4.26171875'], + [1493719865, '4.26171875'], + [1493719925, '4.26171875'], + [1493719985, '4.26171875'], + [1493720045, '4.26171875'], + [1493720105, '4.26171875'], + [1493720165, '4.26171875'], + [1493720225, '4.26171875'], + [1493720285, '4.26171875'], +]; + +export const mockMedian = 1493718485; + +export const mockMedianIndex = 30; |