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