github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/line-chart-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import { 7 find, 8 findAll, 9 click, 10 render, 11 triggerEvent, 12 } from '@ember/test-helpers'; 13 import { module, test } from 'qunit'; 14 import { setupRenderingTest } from 'ember-qunit'; 15 import hbs from 'htmlbars-inline-precompile'; 16 import sinon from 'sinon'; 17 import moment from 'moment'; 18 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 19 20 const REF_DATE = new Date(); 21 22 module('Integration | Component | line-chart', function (hooks) { 23 setupRenderingTest(hooks); 24 25 test('when a chart has annotations, they are rendered in order', async function (assert) { 26 assert.expect(4); 27 28 const annotations = [ 29 { x: 2, type: 'info' }, 30 { x: 1, type: 'error' }, 31 { x: 3, type: 'info' }, 32 ]; 33 this.setProperties({ 34 annotations, 35 data: [ 36 { x: 1, y: 1 }, 37 { x: 10, y: 10 }, 38 ], 39 }); 40 41 await render(hbs` 42 <LineChart 43 @xProp="x" 44 @yProp="y" 45 @data={{this.data}}> 46 <:after as |c|> 47 <c.VAnnotations @annotations={{this.annotations}} /> 48 </:after> 49 </LineChart> 50 `); 51 52 const sortedAnnotations = annotations.sortBy('x'); 53 findAll('[data-test-annotation]').forEach((annotation, idx) => { 54 const datum = sortedAnnotations[idx]; 55 assert.equal( 56 annotation.querySelector('button').getAttribute('title'), 57 `${datum.type} event at ${datum.x}` 58 ); 59 }); 60 61 await componentA11yAudit(this.element, assert); 62 }); 63 64 test('when a chart has annotations and is timeseries, annotations are sorted reverse-chronologically', async function (assert) { 65 assert.expect(3); 66 67 const annotations = [ 68 { 69 x: moment(REF_DATE).add(2, 'd').toDate(), 70 type: 'info', 71 }, 72 { 73 x: moment(REF_DATE).add(1, 'd').toDate(), 74 type: 'error', 75 }, 76 { 77 x: moment(REF_DATE).add(3, 'd').toDate(), 78 type: 'info', 79 }, 80 ]; 81 this.setProperties({ 82 annotations, 83 data: [ 84 { x: 1, y: 1 }, 85 { x: 10, y: 10 }, 86 ], 87 }); 88 89 await render(hbs` 90 <LineChart 91 @xProp="x" 92 @yProp="y" 93 @timeseries={{true}} 94 @data={{this.data}}> 95 <:after as |c|> 96 <c.VAnnotations @annotations={{this.annotations}} /> 97 </:after> 98 </LineChart> 99 `); 100 101 const sortedAnnotations = annotations.sortBy('x').reverse(); 102 findAll('[data-test-annotation]').forEach((annotation, idx) => { 103 const datum = sortedAnnotations[idx]; 104 assert.equal( 105 annotation.querySelector('button').getAttribute('title'), 106 `${datum.type} event at ${moment(datum.x).format('MMM DD, HH:mm')}` 107 ); 108 }); 109 }); 110 111 test('clicking annotations calls the onAnnotationClick action with the annotation as an argument', async function (assert) { 112 const annotations = [{ x: 2, type: 'info', meta: { data: 'here' } }]; 113 this.setProperties({ 114 annotations, 115 data: [ 116 { x: 1, y: 1 }, 117 { x: 10, y: 10 }, 118 ], 119 click: sinon.spy(), 120 }); 121 122 await render(hbs` 123 <LineChart 124 @xProp="x" 125 @yProp="y" 126 @data={{this.data}}> 127 <:after as |c|> 128 <c.VAnnotations @annotations={{this.annotations}} @annotationClick={{this.click}} /> 129 </:after> 130 </LineChart> 131 `); 132 133 await click('[data-test-annotation] button'); 134 assert.ok(this.click.calledWith(annotations[0])); 135 }); 136 137 test('annotations will have staggered heights when too close to be positioned side-by-side', async function (assert) { 138 assert.expect(4); 139 140 const annotations = [ 141 { x: 2, type: 'info' }, 142 { x: 2.4, type: 'error' }, 143 { x: 9, type: 'info' }, 144 ]; 145 this.setProperties({ 146 annotations, 147 data: [ 148 { x: 1, y: 1 }, 149 { x: 10, y: 10 }, 150 ], 151 click: sinon.spy(), 152 }); 153 154 await render(hbs` 155 <div style="width:200px;"> 156 <LineChart 157 @xProp="x" 158 @yProp="y" 159 @data={{this.data}}> 160 <:after as |c|> 161 <c.VAnnotations @annotations={{this.annotations}} /> 162 </:after> 163 </LineChart> 164 </div> 165 `); 166 167 const annotationEls = findAll('[data-test-annotation]'); 168 assert.notOk(annotationEls[0].classList.contains('is-staggered')); 169 assert.ok(annotationEls[1].classList.contains('is-staggered')); 170 assert.notOk(annotationEls[2].classList.contains('is-staggered')); 171 172 await componentA11yAudit(this.element, assert); 173 }); 174 175 test('horizontal annotations render in order', async function (assert) { 176 assert.expect(3); 177 178 const annotations = [ 179 { y: 2, label: 'label one' }, 180 { y: 9, label: 'label three' }, 181 { y: 2.4, label: 'label two' }, 182 ]; 183 this.setProperties({ 184 annotations, 185 data: [ 186 { x: 1, y: 1 }, 187 { x: 10, y: 10 }, 188 ], 189 }); 190 191 await render(hbs` 192 <LineChart 193 @xProp="x" 194 @yProp="y" 195 @data={{this.data}}> 196 <:after as |c|> 197 <c.HAnnotations @annotations={{this.annotations}} @labelProp="label" /> 198 </:after> 199 </LineChart> 200 `); 201 202 const annotationEls = findAll('[data-test-annotation]'); 203 annotations 204 .sortBy('y') 205 .reverse() 206 .forEach((annotation, index) => { 207 assert.equal(annotationEls[index].textContent.trim(), annotation.label); 208 }); 209 }); 210 211 test('the tooltip includes information on the data closest to the mouse', async function (assert) { 212 assert.expect(8); 213 214 const series1 = [ 215 { x: 1, y: 2 }, 216 { x: 3, y: 3 }, 217 { x: 5, y: 4 }, 218 ]; 219 const series2 = [ 220 { x: 2, y: 10 }, 221 { x: 4, y: 9 }, 222 { x: 6, y: 8 }, 223 ]; 224 this.setProperties({ 225 data: [ 226 { series: 'One', data: series1 }, 227 { series: 'Two', data: series2 }, 228 ], 229 }); 230 231 await render(hbs` 232 <div style="width:500px;margin-top:100px"> 233 <LineChart 234 @xProp="x" 235 @yProp="y" 236 @dataProp="data" 237 @data={{this.data}}> 238 <:svg as |c|> 239 {{#each this.data as |series idx|}} 240 <c.Area @data={{series.data}} @colorScale="blues" @index={{idx}} /> 241 {{/each}} 242 </:svg> 243 <:after as |c|> 244 <c.Tooltip as |series datum index|> 245 <li> 246 <span class="label"><span class="color-swatch swatch-blues swatch-blues-{{index}}" />{{series.series}}</span> 247 <span class="value">{{datum.formattedY}}</span> 248 </li> 249 </c.Tooltip> 250 </:after> 251 </LineChart> 252 </div> 253 `); 254 255 // All tooltip events are attached to the hover target 256 const hoverTarget = find('[data-test-hover-target]'); 257 258 // Mouse to data mapping happens based on the clientX of the MouseEvent 259 const bbox = hoverTarget.getBoundingClientRect(); 260 // The MouseEvent needs to be translated based on the location of the hover target 261 const xOffset = bbox.x; 262 // An interval here is the width between x values given the fixed dimensions of the line chart 263 // and the domain of the data 264 const interval = bbox.width / 5; 265 266 // MouseEnter triggers the tooltip visibility 267 await triggerEvent(hoverTarget, 'mouseenter'); 268 // MouseMove positions the tooltip and updates the active datum 269 await triggerEvent(hoverTarget, 'mousemove', { 270 clientX: xOffset + interval * 1 + 5, 271 }); 272 assert.equal(findAll('[data-test-chart-tooltip] li').length, 1); 273 assert.equal( 274 find('[data-test-chart-tooltip] .label').textContent.trim(), 275 this.data[1].series 276 ); 277 assert.equal( 278 find('[data-test-chart-tooltip] .value').textContent.trim(), 279 series2.find((d) => d.x === 2).y 280 ); 281 282 // When the mouse falls between points and each series has points with different x values, 283 // points will only be shown in the tooltip if they are close enough to the closest point 284 // to the cursor. 285 // This event is intentionally between points such that both points are within proximity. 286 const expected = [ 287 { label: this.data[0].series, value: series1.find((d) => d.x === 3).y }, 288 { label: this.data[1].series, value: series2.find((d) => d.x === 2).y }, 289 ]; 290 await triggerEvent(hoverTarget, 'mousemove', { 291 clientX: xOffset + interval * 1.5 + 5, 292 }); 293 assert.equal(findAll('[data-test-chart-tooltip] li').length, 2); 294 findAll('[data-test-chart-tooltip] li').forEach((tooltipEntry, index) => { 295 assert.equal( 296 tooltipEntry.querySelector('.label').textContent.trim(), 297 expected[index].label 298 ); 299 assert.equal( 300 tooltipEntry.querySelector('.value').textContent.trim(), 301 expected[index].value 302 ); 303 }); 304 }); 305 });