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