github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/integration/components/das/recommendation-card-test.js (about) 1 import { module, test } from 'qunit'; 2 import { setupRenderingTest } from 'ember-qunit'; 3 import { render, settled } from '@ember/test-helpers'; 4 import { hbs } from 'ember-cli-htmlbars'; 5 import Service from '@ember/service'; 6 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 7 8 import RecommendationCardComponent from 'nomad-ui/tests/pages/components/recommendation-card'; 9 import { create } from 'ember-cli-page-object'; 10 const RecommendationCard = create(RecommendationCardComponent); 11 12 import { tracked } from '@glimmer/tracking'; 13 import { action } from '@ember/object'; 14 import { set } from '@ember/object'; 15 16 module('Integration | Component | das/recommendation-card', function (hooks) { 17 setupRenderingTest(hooks); 18 19 hooks.beforeEach(function () { 20 const mockRouter = Service.extend({ 21 init() { 22 this._super(...arguments); 23 }, 24 25 urlFor(route, slug, { queryParams: { namespace } }) { 26 return `${route}:${slug}?namespace=${namespace}`; 27 }, 28 }); 29 30 this.owner.register('service:router', mockRouter); 31 }); 32 33 test('it renders a recommendation card', async function (assert) { 34 assert.expect(49); 35 36 const task1 = { 37 name: 'jortle', 38 reservedCPU: 150, 39 reservedMemory: 128, 40 }; 41 42 const task2 = { 43 name: 'tortle', 44 reservedCPU: 125, 45 reservedMemory: 256, 46 }; 47 48 this.set( 49 'summary', 50 new MockRecommendationSummary({ 51 jobNamespace: 'namespace', 52 recommendations: [ 53 { 54 resource: 'MemoryMB', 55 stats: {}, 56 task: task1, 57 value: 192, 58 currentValue: task1.reservedMemory, 59 }, 60 { 61 resource: 'CPU', 62 stats: {}, 63 task: task1, 64 value: 50, 65 currentValue: task1.reservedCPU, 66 }, 67 { 68 resource: 'CPU', 69 stats: {}, 70 task: task2, 71 value: 150, 72 currentValue: task2.reservedCPU, 73 }, 74 { 75 resource: 'MemoryMB', 76 stats: {}, 77 task: task2, 78 value: 320, 79 currentValue: task2.reservedMemory, 80 }, 81 ], 82 83 taskGroup: { 84 count: 2, 85 name: 'group-name', 86 job: { 87 name: 'job-name', 88 namespace: { 89 name: 'namespace', 90 }, 91 }, 92 reservedCPU: task1.reservedCPU + task2.reservedCPU, 93 reservedMemory: task1.reservedMemory + task2.reservedMemory, 94 }, 95 }) 96 ); 97 98 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 99 100 assert.equal(RecommendationCard.slug.jobName, 'job-name'); 101 assert.equal(RecommendationCard.slug.groupName, 'group-name'); 102 103 assert.equal(RecommendationCard.namespace, 'namespace'); 104 105 assert.equal(RecommendationCard.totalsTable.current.cpu.text, '275 MHz'); 106 assert.equal(RecommendationCard.totalsTable.current.memory.text, '384 MiB'); 107 108 RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => { 109 assert.equal(RecommendedCpu.text, '200 MHz'); 110 assert.ok(RecommendedCpu.isDecrease); 111 }); 112 113 RecommendationCard.totalsTable.recommended.memory.as( 114 (RecommendedMemory) => { 115 assert.equal(RecommendedMemory.text, '512 MiB'); 116 assert.ok(RecommendedMemory.isIncrease); 117 } 118 ); 119 120 assert.equal(RecommendationCard.totalsTable.unitDiff.cpu, '-75 MHz'); 121 assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '+128 MiB'); 122 123 // Expected signal has a minus character, not a hyphen. 124 assert.equal(RecommendationCard.totalsTable.percentDiff.cpu, '−27%'); 125 assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+33%'); 126 127 assert.equal(RecommendationCard.copyButton.text, 'job-name / group-name'); 128 assert.ok( 129 RecommendationCard.copyButton.clipboardText.endsWith( 130 'optimize.summary:job-name/group-name?namespace=namespace' 131 ) 132 ); 133 134 assert.equal( 135 RecommendationCard.activeTask.totalsTable.current.cpu.text, 136 '150 MHz' 137 ); 138 assert.equal( 139 RecommendationCard.activeTask.totalsTable.current.memory.text, 140 '128 MiB' 141 ); 142 143 RecommendationCard.activeTask.totalsTable.recommended.cpu.as( 144 (RecommendedCpu) => { 145 assert.equal(RecommendedCpu.text, '50 MHz'); 146 assert.ok(RecommendedCpu.isDecrease); 147 } 148 ); 149 150 RecommendationCard.activeTask.totalsTable.recommended.memory.as( 151 (RecommendedMemory) => { 152 assert.equal(RecommendedMemory.text, '192 MiB'); 153 assert.ok(RecommendedMemory.isIncrease); 154 } 155 ); 156 157 assert.equal(RecommendationCard.activeTask.charts.length, 2); 158 assert.equal( 159 RecommendationCard.activeTask.charts[0].resource, 160 'CPU', 161 'CPU chart should be first when present' 162 ); 163 164 assert.ok(RecommendationCard.activeTask.cpuChart.isDecrease); 165 assert.ok(RecommendationCard.activeTask.memoryChart.isIncrease); 166 167 assert.equal(RecommendationCard.togglesTable.tasks.length, 2); 168 169 await RecommendationCard.togglesTable.tasks[0].as(async (FirstTask) => { 170 assert.equal(FirstTask.name, 'jortle'); 171 assert.ok(FirstTask.isActive); 172 173 assert.equal(FirstTask.cpu.title, 'CPU for jortle'); 174 assert.ok(FirstTask.cpu.isActive); 175 176 assert.equal(FirstTask.memory.title, 'Memory for jortle'); 177 assert.ok(FirstTask.memory.isActive); 178 179 await FirstTask.cpu.toggle(); 180 181 assert.notOk(FirstTask.cpu.isActive); 182 assert.ok(RecommendationCard.activeTask.cpuChart.isDisabled); 183 }); 184 185 assert.notOk(RecommendationCard.togglesTable.tasks[1].isActive); 186 187 assert.equal(RecommendationCard.activeTask.name, 'jortle task'); 188 189 RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => { 190 assert.equal(RecommendedCpu.text, '300 MHz'); 191 assert.ok(RecommendedCpu.isIncrease); 192 }); 193 194 RecommendationCard.activeTask.totalsTable.recommended.cpu.as( 195 (RecommendedCpu) => { 196 assert.equal(RecommendedCpu.text, '150 MHz'); 197 assert.ok(RecommendedCpu.isNeutral); 198 } 199 ); 200 201 await RecommendationCard.togglesTable.toggleAllMemory.toggle(); 202 203 assert.notOk(RecommendationCard.togglesTable.tasks[0].memory.isActive); 204 assert.notOk(RecommendationCard.togglesTable.tasks[1].memory.isActive); 205 206 RecommendationCard.totalsTable.recommended.memory.as( 207 (RecommendedMemory) => { 208 assert.equal(RecommendedMemory.text, '384 MiB'); 209 assert.ok(RecommendedMemory.isNeutral); 210 } 211 ); 212 213 await RecommendationCard.togglesTable.tasks[1].click(); 214 215 assert.notOk(RecommendationCard.togglesTable.tasks[0].isActive); 216 assert.ok(RecommendationCard.togglesTable.tasks[1].isActive); 217 218 assert.equal(RecommendationCard.activeTask.name, 'tortle task'); 219 assert.equal( 220 RecommendationCard.activeTask.totalsTable.current.cpu.text, 221 '125 MHz' 222 ); 223 224 await componentA11yAudit(this.element, assert); 225 }); 226 227 test('it doesn’t have header toggles when there’s only one task', async function (assert) { 228 const task1 = { 229 name: 'jortle', 230 reservedCPU: 150, 231 reservedMemory: 128, 232 }; 233 234 this.set( 235 'summary', 236 new MockRecommendationSummary({ 237 recommendations: [ 238 { 239 resource: 'CPU', 240 stats: {}, 241 task: task1, 242 value: 50, 243 }, 244 { 245 resource: 'MemoryMB', 246 stats: {}, 247 task: task1, 248 value: 192, 249 }, 250 ], 251 252 taskGroup: { 253 count: 1, 254 reservedCPU: task1.reservedCPU, 255 reservedMemory: task1.reservedMemory, 256 }, 257 }) 258 ); 259 260 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 261 262 assert.notOk(RecommendationCard.togglesTable.toggleAllIsPresent); 263 assert.notOk(RecommendationCard.togglesTable.toggleAllCPU.isPresent); 264 assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isPresent); 265 }); 266 267 test('it disables the accept button when all recommendations are disabled', async function (assert) { 268 const task1 = { 269 name: 'jortle', 270 reservedCPU: 150, 271 reservedMemory: 128, 272 }; 273 274 this.set( 275 'summary', 276 new MockRecommendationSummary({ 277 recommendations: [ 278 { 279 resource: 'CPU', 280 stats: {}, 281 task: task1, 282 value: 50, 283 }, 284 { 285 resource: 'MemoryMB', 286 stats: {}, 287 task: task1, 288 value: 192, 289 }, 290 ], 291 292 taskGroup: { 293 count: 1, 294 reservedCPU: task1.reservedCPU, 295 reservedMemory: task1.reservedMemory, 296 }, 297 }) 298 ); 299 300 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 301 302 await RecommendationCard.togglesTable.tasks[0].cpu.toggle(); 303 await RecommendationCard.togglesTable.tasks[0].memory.toggle(); 304 305 assert.ok(RecommendationCard.acceptButton.isDisabled); 306 }); 307 308 test('it doesn’t show a toggle or chart when there’s no recommendation for that resource', async function (assert) { 309 const task1 = { 310 name: 'jortle', 311 reservedCPU: 150, 312 reservedMemory: 128, 313 }; 314 315 this.set( 316 'summary', 317 new MockRecommendationSummary({ 318 recommendations: [ 319 { 320 resource: 'CPU', 321 stats: {}, 322 task: task1, 323 value: 50, 324 }, 325 ], 326 327 taskGroup: { 328 count: 2, 329 name: 'group-name', 330 job: { 331 name: 'job-name', 332 }, 333 reservedCPU: task1.reservedCPU, 334 reservedMemory: task1.reservedMemory, 335 }, 336 }) 337 ); 338 339 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 340 341 assert.equal( 342 RecommendationCard.totalsTable.recommended.memory.text, 343 '128 MiB' 344 ); 345 assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '0 MiB'); 346 assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+0%'); 347 348 assert.equal( 349 RecommendationCard.narrative.trim(), 350 'Applying the selected recommendations will save an aggregate 200 MHz of CPU across 2 allocations.' 351 ); 352 353 assert.ok(RecommendationCard.togglesTable.tasks[0].memory.isDisabled); 354 assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent); 355 }); 356 357 test('it disables a resource’s toggle all toggle when there are no recommendations for it', async function (assert) { 358 const task1 = { 359 name: 'jortle', 360 reservedCPU: 150, 361 reservedMemory: 128, 362 }; 363 364 const task2 = { 365 name: 'tortle', 366 reservedCPU: 150, 367 reservedMemory: 128, 368 }; 369 370 this.set( 371 'summary', 372 new MockRecommendationSummary({ 373 recommendations: [ 374 { 375 resource: 'CPU', 376 stats: {}, 377 task: task1, 378 value: 50, 379 }, 380 { 381 resource: 'CPU', 382 stats: {}, 383 task: task2, 384 value: 50, 385 }, 386 ], 387 388 taskGroup: { 389 count: 2, 390 name: 'group-name', 391 job: { 392 name: 'job-name', 393 }, 394 reservedCPU: task1.reservedCPU + task2.reservedCPU, 395 reservedMemory: task1.reservedMemory + task2.reservedMemory, 396 }, 397 }) 398 ); 399 400 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 401 402 assert.ok(RecommendationCard.togglesTable.toggleAllMemory.isDisabled); 403 assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isActive); 404 assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent); 405 }); 406 407 test('it renders diff calculations in a sentence', async function (assert) { 408 const task1 = { 409 name: 'jortle', 410 reservedCPU: 150, 411 reservedMemory: 128, 412 }; 413 414 const task2 = { 415 name: 'tortle', 416 reservedCPU: 125, 417 reservedMemory: 256, 418 }; 419 420 this.set( 421 'summary', 422 new MockRecommendationSummary({ 423 recommendations: [ 424 { 425 resource: 'CPU', 426 stats: {}, 427 task: task1, 428 value: 50, 429 currentValue: task1.reservedCPU, 430 }, 431 { 432 resource: 'MemoryMB', 433 stats: {}, 434 task: task1, 435 value: 192, 436 currentValue: task1.reservedMemory, 437 }, 438 { 439 resource: 'CPU', 440 stats: {}, 441 task: task2, 442 value: 150, 443 currentValue: task2.reservedCPU, 444 }, 445 { 446 resource: 'MemoryMB', 447 stats: {}, 448 task: task2, 449 value: 320, 450 currentValue: task2.reservedMemory, 451 }, 452 ], 453 454 taskGroup: { 455 count: 10, 456 name: 'group-name', 457 job: { 458 name: 'job-name', 459 namespace: { 460 name: 'namespace', 461 }, 462 }, 463 reservedCPU: task1.reservedCPU + task2.reservedCPU, 464 reservedMemory: task1.reservedMemory + task2.reservedMemory, 465 }, 466 }) 467 ); 468 469 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 470 471 const [cpuRec1, memRec1, cpuRec2, memRec2] = this.summary.recommendations; 472 473 assert.equal( 474 RecommendationCard.narrative.trim(), 475 'Applying the selected recommendations will save an aggregate 750 MHz of CPU and add an aggregate 1.25 GiB of memory across 10 allocations.' 476 ); 477 478 this.summary.toggleRecommendation(cpuRec1); 479 await settled(); 480 481 assert.equal( 482 RecommendationCard.narrative.trim(), 483 'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 1.25 GiB of memory across 10 allocations.' 484 ); 485 486 this.summary.toggleRecommendation(memRec1); 487 await settled(); 488 489 assert.equal( 490 RecommendationCard.narrative.trim(), 491 'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 640 MiB of memory across 10 allocations.' 492 ); 493 494 this.summary.toggleRecommendation(cpuRec2); 495 await settled(); 496 497 assert.equal( 498 RecommendationCard.narrative.trim(), 499 'Applying the selected recommendations will add an aggregate 640 MiB of memory across 10 allocations.' 500 ); 501 502 this.summary.toggleRecommendation(cpuRec1); 503 this.summary.toggleRecommendation(memRec2); 504 await settled(); 505 506 assert.equal( 507 RecommendationCard.narrative.trim(), 508 'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.' 509 ); 510 511 this.summary.toggleRecommendation(cpuRec1); 512 await settled(); 513 514 assert.equal(RecommendationCard.narrative.trim(), ''); 515 516 this.summary.toggleRecommendation(cpuRec1); 517 await settled(); 518 519 assert.equal( 520 RecommendationCard.narrative.trim(), 521 'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.' 522 ); 523 524 this.summary.toggleRecommendation(memRec2); 525 set(memRec2, 'value', 128); 526 await settled(); 527 528 assert.equal( 529 RecommendationCard.narrative.trim(), 530 'Applying the selected recommendations will save an aggregate 1 GHz of CPU and 1.25 GiB of memory across 10 allocations.' 531 ); 532 }); 533 534 test('it renders diff calculations in a sentence with no aggregation for one allocatio', async function (assert) { 535 const task1 = { 536 name: 'jortle', 537 reservedCPU: 150, 538 reservedMemory: 128, 539 }; 540 541 const task2 = { 542 name: 'tortle', 543 reservedCPU: 125, 544 reservedMemory: 256, 545 }; 546 547 this.set( 548 'summary', 549 new MockRecommendationSummary({ 550 recommendations: [ 551 { 552 resource: 'CPU', 553 stats: {}, 554 task: task1, 555 value: 50, 556 currentValue: task1.reservedCPU, 557 }, 558 { 559 resource: 'MemoryMB', 560 stats: {}, 561 task: task1, 562 value: 192, 563 currentValue: task1.reservedMemory, 564 }, 565 { 566 resource: 'CPU', 567 stats: {}, 568 task: task2, 569 value: 150, 570 currentValue: task2.reservedCPU, 571 }, 572 { 573 resource: 'MemoryMB', 574 stats: {}, 575 task: task2, 576 value: 320, 577 currentValue: task2.reservedMemory, 578 }, 579 ], 580 581 taskGroup: { 582 count: 1, 583 name: 'group-name', 584 job: { 585 name: 'job-name', 586 namespace: { 587 name: 'namespace', 588 }, 589 }, 590 reservedCPU: task1.reservedCPU + task2.reservedCPU, 591 reservedMemory: task1.reservedMemory + task2.reservedMemory, 592 }, 593 }) 594 ); 595 596 await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`); 597 598 assert.equal( 599 RecommendationCard.narrative.trim(), 600 'Applying the selected recommendations will save 75 MHz of CPU and add 128 MiB of memory.' 601 ); 602 }); 603 }); 604 605 class MockRecommendationSummary { 606 @tracked excludedRecommendations = []; 607 608 constructor(attributes) { 609 Object.assign(this, attributes); 610 } 611 612 get slug() { 613 return `${this.taskGroup?.job?.name}/${this.taskGroup?.name}`; 614 } 615 616 @action 617 toggleRecommendation(recommendation) { 618 if (this.excludedRecommendations.includes(recommendation)) { 619 this.excludedRecommendations.removeObject(recommendation); 620 } else { 621 this.excludedRecommendations.pushObject(recommendation); 622 } 623 } 624 625 @action 626 toggleAllRecommendationsForResource(resource, enabled) { 627 if (enabled) { 628 this.excludedRecommendations = this.excludedRecommendations.rejectBy( 629 'resource', 630 resource 631 ); 632 } else { 633 this.excludedRecommendations.pushObjects( 634 this.recommendations.filterBy('resource', resource) 635 ); 636 } 637 } 638 }