github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/integration/components/topo-viz/node-test.js (about) 1 import { findAll, render } from '@ember/test-helpers'; 2 import { module, test } from 'qunit'; 3 import { setupRenderingTest } from 'ember-qunit'; 4 import hbs from 'htmlbars-inline-precompile'; 5 import { create } from 'ember-cli-page-object'; 6 import sinon from 'sinon'; 7 import faker from 'nomad-ui/mirage/faker'; 8 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 9 import { setupMirage } from 'ember-cli-mirage/test-support'; 10 import topoVisNodePageObject from 'nomad-ui/tests/pages/components/topo-viz/node'; 11 import { 12 formatScheduledBytes, 13 formatScheduledHertz, 14 } from 'nomad-ui/utils/units'; 15 16 const TopoVizNode = create(topoVisNodePageObject()); 17 18 const nodeGen = (name, datacenter, memory, cpu, flags = {}) => ({ 19 datacenter, 20 memory, 21 cpu, 22 isSelected: !!flags.isSelected, 23 node: { 24 name, 25 isEligible: flags.isEligible || flags.isEligible == null, 26 isDraining: !!flags.isDraining, 27 }, 28 }); 29 30 const allocGen = (node, memory, cpu, isScheduled = true) => ({ 31 memory, 32 cpu, 33 isSelected: false, 34 memoryPercent: memory / node.memory, 35 cpuPercent: cpu / node.cpu, 36 allocation: { 37 id: faker.random.uuid(), 38 isScheduled, 39 }, 40 }); 41 42 const props = (overrides) => ({ 43 isDense: false, 44 heightScale: () => 50, 45 onAllocationSelect: sinon.spy(), 46 onNodeSelect: sinon.spy(), 47 onAllocationFocus: sinon.spy(), 48 onAllocationBlur: sinon.spy(), 49 ...overrides, 50 }); 51 52 module('Integration | Component | TopoViz::Node', function (hooks) { 53 setupRenderingTest(hooks); 54 setupMirage(hooks); 55 56 const commonTemplate = hbs` 57 <TopoViz::Node 58 @node={{this.node}} 59 @isDense={{this.isDense}} 60 @heightScale={{this.heightScale}} 61 @onAllocationSelect={{this.onAllocationSelect}} 62 @onAllocationFocus={{this.onAllocationFocus}} 63 @onAllocationBlur={{this.onAllocationBlur}} 64 @onNodeSelect={{this.onNodeSelect}} /> 65 `; 66 67 test('presents as a div with a label and an svg with CPU and memory rows', async function (assert) { 68 assert.expect(4); 69 70 const node = nodeGen('Node One', 'dc1', 1000, 1000); 71 this.setProperties( 72 props({ 73 node: { 74 ...node, 75 allocations: [ 76 allocGen(node, 100, 100), 77 allocGen(node, 250, 250), 78 allocGen(node, 300, 300, false), 79 ], 80 }, 81 }) 82 ); 83 84 await render(commonTemplate); 85 86 assert.ok(TopoVizNode.isPresent); 87 assert.equal( 88 TopoVizNode.memoryRects.length, 89 this.node.allocations.filterBy('allocation.isScheduled').length 90 ); 91 assert.ok(TopoVizNode.cpuRects.length); 92 93 await componentA11yAudit(this.element, assert); 94 }); 95 96 test('the label contains aggregate information about the node', async function (assert) { 97 const node = nodeGen('Node One', 'dc1', 1000, 1000); 98 this.setProperties( 99 props({ 100 node: { 101 ...node, 102 allocations: [ 103 allocGen(node, 100, 100), 104 allocGen(node, 250, 250), 105 allocGen(node, 300, 300, false), 106 ], 107 }, 108 }) 109 ); 110 111 await render(commonTemplate); 112 113 assert.ok(TopoVizNode.label.includes(node.node.name)); 114 assert.ok( 115 TopoVizNode.label.includes( 116 `${ 117 this.node.allocations.filterBy('allocation.isScheduled').length 118 } Allocs` 119 ) 120 ); 121 assert.ok( 122 TopoVizNode.label.includes( 123 `${formatScheduledBytes(this.node.memory, 'MiB')}` 124 ) 125 ); 126 assert.ok( 127 TopoVizNode.label.includes( 128 `${formatScheduledHertz(this.node.cpu, 'MHz')}` 129 ) 130 ); 131 }); 132 133 test('the status icon indicates when the node is draining', async function (assert) { 134 const node = nodeGen('Node One', 'dc1', 1000, 1000, { isDraining: true }); 135 this.setProperties( 136 props({ 137 node: { 138 ...node, 139 allocations: [], 140 }, 141 }) 142 ); 143 144 await render(commonTemplate); 145 146 assert.ok(TopoVizNode.statusIcon.includes('icon-is-clock-outline')); 147 assert.equal(TopoVizNode.statusIconLabel, 'Client is draining'); 148 }); 149 150 test('the status icon indicates when the node is ineligible for scheduling', async function (assert) { 151 const node = nodeGen('Node One', 'dc1', 1000, 1000, { isEligible: false }); 152 this.setProperties( 153 props({ 154 node: { 155 ...node, 156 allocations: [], 157 }, 158 }) 159 ); 160 161 await render(commonTemplate); 162 163 assert.ok(TopoVizNode.statusIcon.includes('icon-is-lock-closed')); 164 assert.equal(TopoVizNode.statusIconLabel, 'Client is ineligible'); 165 }); 166 167 test('when isDense is false, clicking the node does nothing', async function (assert) { 168 const node = nodeGen('Node One', 'dc1', 1000, 1000); 169 this.setProperties( 170 props({ 171 isDense: false, 172 node: { 173 ...node, 174 allocations: [], 175 }, 176 }) 177 ); 178 179 await render(commonTemplate); 180 await TopoVizNode.selectNode(); 181 182 assert.notOk(TopoVizNode.nodeIsInteractive); 183 assert.notOk(this.onNodeSelect.called); 184 }); 185 186 test('when isDense is true, clicking the node calls onNodeSelect', async function (assert) { 187 const node = nodeGen('Node One', 'dc1', 1000, 1000); 188 this.setProperties( 189 props({ 190 isDense: true, 191 node: { 192 ...node, 193 allocations: [], 194 }, 195 }) 196 ); 197 198 await render(commonTemplate); 199 await TopoVizNode.selectNode(); 200 201 assert.ok(TopoVizNode.nodeIsInteractive); 202 assert.ok(this.onNodeSelect.called); 203 assert.ok(this.onNodeSelect.calledWith(this.node)); 204 }); 205 206 test('the node gets the is-selected class when the node is selected', async function (assert) { 207 const node = nodeGen('Node One', 'dc1', 1000, 1000, { isSelected: true }); 208 this.setProperties( 209 props({ 210 isDense: true, 211 node: { 212 ...node, 213 allocations: [], 214 }, 215 }) 216 ); 217 218 await render(commonTemplate); 219 220 assert.ok(TopoVizNode.nodeIsSelected); 221 }); 222 223 test('the node gets its height form the @heightScale arg', async function (assert) { 224 const node = nodeGen('Node One', 'dc1', 1000, 1000); 225 const height = 50; 226 const heightSpy = sinon.spy(); 227 this.setProperties( 228 props({ 229 heightScale: (...args) => { 230 heightSpy(...args); 231 return height; 232 }, 233 node: { 234 ...node, 235 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 236 }, 237 }) 238 ); 239 240 await render(commonTemplate); 241 242 assert.ok(heightSpy.called); 243 assert.ok(heightSpy.calledWith(this.node.memory)); 244 assert.equal(TopoVizNode.memoryRects[0].height, `${height}px`); 245 }); 246 247 test('each allocation gets a memory rect and a cpu rect', async function (assert) { 248 const node = nodeGen('Node One', 'dc1', 1000, 1000); 249 this.setProperties( 250 props({ 251 node: { 252 ...node, 253 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 254 }, 255 }) 256 ); 257 258 await render(commonTemplate); 259 260 assert.equal(TopoVizNode.memoryRects.length, this.node.allocations.length); 261 assert.equal(TopoVizNode.cpuRects.length, this.node.allocations.length); 262 }); 263 264 test('each allocation is sized according to its percentage of utilization', async function (assert) { 265 assert.expect(4); 266 267 const node = nodeGen('Node One', 'dc1', 1000, 1000); 268 this.setProperties( 269 props({ 270 node: { 271 ...node, 272 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 273 }, 274 }) 275 ); 276 277 await render(hbs` 278 <div style="width:100px"> 279 <TopoViz::Node 280 @node={{this.node}} 281 @isDense={{this.isDense}} 282 @heightScale={{this.heightScale}} 283 @onAllocationSelect={{this.onAllocationSelect}} 284 @onNodeSelect={{this.onNodeSelect}} /> 285 </div> 286 `); 287 288 // Remove the width of the padding and the label from the SVG width 289 const width = 100 - 5 - 5 - 20; 290 this.node.allocations.forEach((alloc, index) => { 291 const memWidth = alloc.memoryPercent * width - (index === 0 ? 0.5 : 1); 292 const cpuWidth = alloc.cpuPercent * width - (index === 0 ? 0.5 : 1); 293 assert.equal(TopoVizNode.memoryRects[index].width, `${memWidth}px`); 294 assert.equal(TopoVizNode.cpuRects[index].width, `${cpuWidth}px`); 295 }); 296 }); 297 298 test('clicking either the memory or cpu rect for an allocation will call onAllocationSelect', async function (assert) { 299 const node = nodeGen('Node One', 'dc1', 1000, 1000); 300 this.setProperties( 301 props({ 302 node: { 303 ...node, 304 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 305 }, 306 }) 307 ); 308 309 await render(commonTemplate); 310 311 await TopoVizNode.memoryRects[0].select(); 312 assert.equal(this.onAllocationSelect.callCount, 1); 313 assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[0])); 314 315 await TopoVizNode.cpuRects[0].select(); 316 assert.equal(this.onAllocationSelect.callCount, 2); 317 318 await TopoVizNode.cpuRects[1].select(); 319 assert.equal(this.onAllocationSelect.callCount, 3); 320 assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[1])); 321 322 await TopoVizNode.memoryRects[1].select(); 323 assert.equal(this.onAllocationSelect.callCount, 4); 324 }); 325 326 test('hovering over a memory or cpu rect for an allocation will call onAllocationFocus', async function (assert) { 327 const node = nodeGen('Node One', 'dc1', 1000, 1000); 328 this.setProperties( 329 props({ 330 node: { 331 ...node, 332 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 333 }, 334 }) 335 ); 336 337 await render(commonTemplate); 338 339 await TopoVizNode.memoryRects[0].hover(); 340 assert.equal(this.onAllocationFocus.callCount, 1); 341 assert.equal( 342 this.onAllocationFocus.getCall(0).args[0].allocation, 343 this.node.allocations[0].allocation 344 ); 345 assert.equal( 346 this.onAllocationFocus.getCall(0).args[1], 347 findAll('[data-test-memory-rect]')[0] 348 ); 349 350 await TopoVizNode.cpuRects[1].hover(); 351 assert.equal(this.onAllocationFocus.callCount, 2); 352 assert.equal( 353 this.onAllocationFocus.getCall(1).args[0].allocation, 354 this.node.allocations[1].allocation 355 ); 356 assert.equal( 357 this.onAllocationFocus.getCall(1).args[1], 358 findAll('[data-test-cpu-rect]')[1] 359 ); 360 }); 361 362 test('leaving the entire node will call onAllocationBlur, which allows for the tooltip transitions', async function (assert) { 363 const node = nodeGen('Node One', 'dc1', 1000, 1000); 364 this.setProperties( 365 props({ 366 node: { 367 ...node, 368 allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)], 369 }, 370 }) 371 ); 372 373 await render(commonTemplate); 374 375 await TopoVizNode.memoryRects[0].hover(); 376 assert.equal(this.onAllocationFocus.callCount, 1); 377 assert.equal(this.onAllocationBlur.callCount, 0); 378 379 await TopoVizNode.memoryRects[0].mouseleave(); 380 assert.equal(this.onAllocationBlur.callCount, 0); 381 382 await TopoVizNode.mouseout(); 383 assert.equal(this.onAllocationBlur.callCount, 1); 384 }); 385 386 test('allocations are sorted by smallest to largest delta of memory to cpu percent utilizations', async function (assert) { 387 assert.expect(10); 388 389 const node = nodeGen('Node One', 'dc1', 1000, 1000); 390 391 const evenAlloc = allocGen(node, 100, 100); 392 const mediumMemoryAlloc = allocGen(node, 200, 150); 393 const largeMemoryAlloc = allocGen(node, 300, 50); 394 const mediumCPUAlloc = allocGen(node, 150, 200); 395 const largeCPUAlloc = allocGen(node, 50, 300); 396 397 this.setProperties( 398 props({ 399 node: { 400 ...node, 401 allocations: [ 402 largeCPUAlloc, 403 mediumCPUAlloc, 404 evenAlloc, 405 mediumMemoryAlloc, 406 largeMemoryAlloc, 407 ], 408 }, 409 }) 410 ); 411 412 await render(commonTemplate); 413 414 const expectedOrder = [ 415 evenAlloc, 416 mediumCPUAlloc, 417 mediumMemoryAlloc, 418 largeCPUAlloc, 419 largeMemoryAlloc, 420 ]; 421 expectedOrder.forEach((alloc, index) => { 422 assert.equal(TopoVizNode.memoryRects[index].id, alloc.allocation.id); 423 assert.equal(TopoVizNode.cpuRects[index].id, alloc.allocation.id); 424 }); 425 }); 426 427 test('when there are no allocations, a "no allocations" note is shown', async function (assert) { 428 const node = nodeGen('Node One', 'dc1', 1000, 1000); 429 this.setProperties( 430 props({ 431 node: { 432 ...node, 433 allocations: [], 434 }, 435 }) 436 ); 437 438 await render(commonTemplate); 439 assert.equal(TopoVizNode.emptyMessage, 'Empty Client'); 440 }); 441 });