github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/optimize-test.js (about) 1 import { module, test } from 'qunit'; 2 import { setupApplicationTest } from 'ember-qunit'; 3 import { currentURL, visit } from '@ember/test-helpers'; 4 import { setupMirage } from 'ember-cli-mirage/test-support'; 5 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 6 import Response from 'ember-cli-mirage/response'; 7 import moment from 'moment'; 8 9 import Optimize from 'nomad-ui/tests/pages/optimize'; 10 import Layout from 'nomad-ui/tests/pages/layout'; 11 import JobsList from 'nomad-ui/tests/pages/jobs/list'; 12 13 let managementToken, clientToken; 14 15 function getLatestRecommendationSubmitTimeForJob(job) { 16 const tasks = job.taskGroups.models 17 .mapBy('tasks.models') 18 .reduce((tasks, taskModels) => tasks.concat(taskModels), []); 19 const recommendations = tasks.reduce( 20 (recommendations, task) => recommendations.concat(task.recommendations.models), 21 [] 22 ); 23 return Math.max(...recommendations.mapBy('submitTime')); 24 } 25 26 module('Acceptance | optimize', function(hooks) { 27 setupApplicationTest(hooks); 28 setupMirage(hooks); 29 30 hooks.beforeEach(async function() { 31 server.create('feature', { name: 'Dynamic Application Sizing' }); 32 33 server.create('node'); 34 35 server.createList('namespace', 2); 36 37 const jobs = server.createList('job', 2, { 38 createRecommendations: true, 39 groupsCount: 1, 40 groupTaskCount: 2, 41 namespaceId: server.db.namespaces[1].id, 42 }); 43 44 jobs.sort((jobA, jobB) => { 45 return ( 46 getLatestRecommendationSubmitTimeForJob(jobB) - 47 getLatestRecommendationSubmitTimeForJob(jobA) 48 ); 49 }); 50 51 [this.job1, this.job2] = jobs; 52 53 managementToken = server.create('token'); 54 clientToken = server.create('token'); 55 56 window.localStorage.clear(); 57 window.localStorage.nomadTokenSecret = managementToken.secretId; 58 }); 59 60 test('it passes an accessibility audit', async function(assert) { 61 await Optimize.visit(); 62 await a11yAudit(assert); 63 }); 64 65 test('lets recommendations be toggled, reports the choices to the recommendations API, and displays task group recommendations serially', async function(assert) { 66 const currentTaskGroup = this.job1.taskGroups.models[0]; 67 const nextTaskGroup = this.job2.taskGroups.models[0]; 68 69 const currentTaskGroupHasCPURecommendation = currentTaskGroup.tasks.models 70 .mapBy('recommendations.models') 71 .flat() 72 .find(r => r.resource === 'CPU'); 73 74 // If no CPU recommendation, will not be able to accept recommendation with all memory recommendations turned off 75 76 if (!currentTaskGroupHasCPURecommendation) { 77 const currentTaskGroupTask = currentTaskGroup.tasks.models[0]; 78 this.server.create('recommendation', { 79 task: currentTaskGroupTask, 80 resource: 'CPU', 81 }); 82 } 83 84 await Optimize.visit(); 85 86 assert.equal(Layout.breadcrumbFor('optimize').text, 'Recommendations'); 87 88 assert.equal( 89 Optimize.recommendationSummaries[0].slug, 90 `${this.job1.name} / ${currentTaskGroup.name}` 91 ); 92 93 assert.equal( 94 Layout.breadcrumbFor('optimize.summary').text, 95 `${this.job1.name} / ${currentTaskGroup.name}` 96 ); 97 98 assert.equal(Optimize.recommendationSummaries[0].namespace, this.job1.namespace); 99 100 assert.equal( 101 Optimize.recommendationSummaries[1].slug, 102 `${this.job2.name} / ${nextTaskGroup.name}` 103 ); 104 105 const currentRecommendations = currentTaskGroup.tasks.models.reduce( 106 (recommendations, task) => recommendations.concat(task.recommendations.models), 107 [] 108 ); 109 const latestSubmitTime = Math.max(...currentRecommendations.mapBy('submitTime')); 110 111 Optimize.recommendationSummaries[0].as(summary => { 112 assert.equal( 113 summary.date, 114 moment(new Date(latestSubmitTime / 1000000)).format('MMM DD HH:mm:ss ZZ') 115 ); 116 117 const currentTaskGroupAllocations = server.schema.allocations.where({ 118 jobId: currentTaskGroup.job.name, 119 taskGroup: currentTaskGroup.name, 120 }); 121 assert.equal(summary.allocationCount, currentTaskGroupAllocations.length); 122 123 const { currCpu, currMem } = currentTaskGroup.tasks.models.reduce( 124 (currentResources, task) => { 125 currentResources.currCpu += task.resources.CPU; 126 currentResources.currMem += task.resources.MemoryMB; 127 return currentResources; 128 }, 129 { currCpu: 0, currMem: 0 } 130 ); 131 132 const { recCpu, recMem } = currentRecommendations.reduce( 133 (recommendedResources, recommendation) => { 134 if (recommendation.resource === 'CPU') { 135 recommendedResources.recCpu += recommendation.value; 136 } else { 137 recommendedResources.recMem += recommendation.value; 138 } 139 140 return recommendedResources; 141 }, 142 { recCpu: 0, recMem: 0 } 143 ); 144 145 const cpuDiff = recCpu > 0 ? recCpu - currCpu : 0; 146 const memDiff = recMem > 0 ? recMem - currMem : 0; 147 148 const cpuSign = cpuDiff > 0 ? '+' : ''; 149 const memSign = memDiff > 0 ? '+' : ''; 150 151 const cpuDiffPercent = Math.round((100 * cpuDiff) / currCpu); 152 const memDiffPercent = Math.round((100 * memDiff) / currMem); 153 154 assert.equal( 155 summary.cpu, 156 cpuDiff ? `${cpuSign}${cpuDiff} MHz ${cpuSign}${cpuDiffPercent}%` : '' 157 ); 158 assert.equal( 159 summary.memory, 160 memDiff ? `${memSign}${formattedMemDiff(memDiff)} ${memSign}${memDiffPercent}%` : '' 161 ); 162 163 assert.equal( 164 summary.aggregateCpu, 165 cpuDiff ? `${cpuSign}${cpuDiff * currentTaskGroupAllocations.length} MHz` : '' 166 ); 167 168 assert.equal( 169 summary.aggregateMemory, 170 memDiff ? `${memSign}${formattedMemDiff(memDiff * currentTaskGroupAllocations.length)}` : '' 171 ); 172 }); 173 174 assert.ok(Optimize.recommendationSummaries[0].isActive); 175 assert.notOk(Optimize.recommendationSummaries[1].isActive); 176 177 assert.equal(Optimize.card.slug.jobName, this.job1.name); 178 assert.equal(Optimize.card.slug.groupName, currentTaskGroup.name); 179 180 const summaryMemoryBefore = Optimize.recommendationSummaries[0].memory; 181 182 let toggledAnything = true; 183 184 // Toggle off all memory 185 if (Optimize.card.togglesTable.toggleAllMemory.isPresent) { 186 await Optimize.card.togglesTable.toggleAllMemory.toggle(); 187 188 assert.notOk(Optimize.card.togglesTable.tasks[0].memory.isActive); 189 assert.notOk(Optimize.card.togglesTable.tasks[1].memory.isActive); 190 } else if (!Optimize.card.togglesTable.tasks[0].cpu.isDisabled) { 191 await Optimize.card.togglesTable.tasks[0].memory.toggle(); 192 } else { 193 toggledAnything = false; 194 } 195 196 assert.equal( 197 Optimize.recommendationSummaries[0].memory, 198 summaryMemoryBefore, 199 'toggling recommendations doesn’t affect the summary table diffs' 200 ); 201 202 const currentTaskIds = currentTaskGroup.tasks.models.mapBy('id'); 203 const taskIdFilter = task => currentTaskIds.includes(task.taskId); 204 205 const cpuRecommendationIds = server.schema.recommendations 206 .where({ resource: 'CPU' }) 207 .models.filter(taskIdFilter) 208 .mapBy('id'); 209 210 const memoryRecommendationIds = server.schema.recommendations 211 .where({ resource: 'MemoryMB' }) 212 .models.filter(taskIdFilter) 213 .mapBy('id'); 214 215 const appliedIds = toggledAnything ? cpuRecommendationIds : memoryRecommendationIds; 216 const dismissedIds = toggledAnything ? memoryRecommendationIds : []; 217 218 await Optimize.card.acceptButton.click(); 219 220 const request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 221 const { Apply, Dismiss } = JSON.parse(request.requestBody); 222 223 assert.equal(request.url, '/v1/recommendations/apply'); 224 225 assert.deepEqual(Apply, appliedIds); 226 assert.deepEqual(Dismiss, dismissedIds); 227 228 assert.equal(Optimize.card.slug.jobName, this.job2.name); 229 assert.equal(Optimize.card.slug.groupName, nextTaskGroup.name); 230 231 assert.ok(Optimize.recommendationSummaries[1].isActive); 232 }); 233 234 test('can navigate between summaries via the table', async function(assert) { 235 server.createList('job', 10, { 236 createRecommendations: true, 237 groupsCount: 1, 238 groupTaskCount: 2, 239 namespaceId: server.db.namespaces[1].id, 240 }); 241 242 await Optimize.visit(); 243 await Optimize.recommendationSummaries[1].click(); 244 245 assert.equal( 246 `${Optimize.card.slug.jobName} / ${Optimize.card.slug.groupName}`, 247 Optimize.recommendationSummaries[1].slug 248 ); 249 assert.ok(Optimize.recommendationSummaries[1].isActive); 250 }); 251 252 test('can visit a summary directly via URL', async function(assert) { 253 server.createList('job', 10, { 254 createRecommendations: true, 255 groupsCount: 1, 256 groupTaskCount: 2, 257 namespaceId: server.db.namespaces[1].id, 258 }); 259 260 await Optimize.visit(); 261 262 const lastSummary = 263 Optimize.recommendationSummaries[Optimize.recommendationSummaries.length - 1]; 264 const collapsedSlug = lastSummary.slug.replace(' / ', '/'); 265 266 // preferable to use page object’s visitable but it encodes the slash 267 await visit(`/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`); 268 269 assert.equal( 270 `${Optimize.card.slug.jobName} / ${Optimize.card.slug.groupName}`, 271 lastSummary.slug 272 ); 273 assert.ok(lastSummary.isActive); 274 assert.equal(currentURL(), `/optimize/${collapsedSlug}?namespace=${lastSummary.namespace}`); 275 }); 276 277 test('when a summary is not found, an error message is shown, but the URL persists', async function(assert) { 278 await visit('/optimize/nonexistent/summary?namespace=anamespace'); 279 280 assert.equal(currentURL(), '/optimize/nonexistent/summary?namespace=anamespace'); 281 assert.ok(Optimize.applicationError.isPresent); 282 assert.equal(Optimize.applicationError.title, 'Not Found'); 283 }); 284 285 test('cannot return to already-processed summaries', async function(assert) { 286 await Optimize.visit(); 287 await Optimize.card.acceptButton.click(); 288 289 assert.ok(Optimize.recommendationSummaries[0].isDisabled); 290 291 await Optimize.recommendationSummaries[0].click(); 292 293 assert.ok(Optimize.recommendationSummaries[1].isActive); 294 }); 295 296 test('can dismiss a set of recommendations', async function(assert) { 297 await Optimize.visit(); 298 299 const currentTaskGroup = this.job1.taskGroups.models[0]; 300 const currentTaskIds = currentTaskGroup.tasks.models.mapBy('id'); 301 const taskIdFilter = task => currentTaskIds.includes(task.taskId); 302 303 const idsBeforeDismissal = server.schema.recommendations 304 .all() 305 .models.filter(taskIdFilter) 306 .mapBy('id'); 307 308 await Optimize.card.dismissButton.click(); 309 310 const request = server.pretender.handledRequests.filterBy('method', 'POST').pop(); 311 const { Apply, Dismiss } = JSON.parse(request.requestBody); 312 313 assert.equal(request.url, '/v1/recommendations/apply'); 314 315 assert.deepEqual(Apply, []); 316 assert.deepEqual(Dismiss, idsBeforeDismissal); 317 }); 318 319 test('it displays an error encountered trying to save and proceeds to the next summary when the error is dismissed', async function(assert) { 320 server.post('/recommendations/apply', function() { 321 return new Response(500, {}, null); 322 }); 323 324 await Optimize.visit(); 325 await Optimize.card.acceptButton.click(); 326 327 assert.ok(Optimize.error.isPresent); 328 assert.equal(Optimize.error.headline, 'Recommendation error'); 329 assert.equal( 330 Optimize.error.errors, 331 'Error: Ember Data Request POST /v1/recommendations/apply returned a 500 Payload (application/json)' 332 ); 333 334 await Optimize.error.dismiss(); 335 assert.equal(Optimize.card.slug.jobName, this.job2.name); 336 }); 337 338 test('it displays an empty message when there are no recommendations', async function(assert) { 339 server.db.recommendations.remove(); 340 await Optimize.visit(); 341 342 assert.ok(Optimize.empty.isPresent); 343 assert.equal(Optimize.empty.headline, 'No Recommendations'); 344 }); 345 346 test('it displays an empty message after all recommendations have been processed', async function(assert) { 347 await Optimize.visit(); 348 349 await Optimize.card.acceptButton.click(); 350 await Optimize.card.acceptButton.click(); 351 352 assert.ok(Optimize.empty.isPresent); 353 }); 354 355 test('it redirects to jobs and hides the gutter link when the token lacks permissions', async function(assert) { 356 window.localStorage.nomadTokenSecret = clientToken.secretId; 357 await Optimize.visit(); 358 359 assert.equal(currentURL(), '/jobs'); 360 assert.ok(Layout.gutter.optimize.isHidden); 361 }); 362 363 test('it reloads partially-loaded jobs', async function(assert) { 364 await JobsList.visit(); 365 await Optimize.visit(); 366 367 assert.equal(Optimize.recommendationSummaries.length, 2); 368 }); 369 }); 370 371 module('Acceptance | optimize search and facets', function(hooks) { 372 setupApplicationTest(hooks); 373 setupMirage(hooks); 374 375 hooks.beforeEach(async function() { 376 server.create('feature', { name: 'Dynamic Application Sizing' }); 377 378 server.create('node'); 379 380 server.createList('namespace', 2); 381 382 managementToken = server.create('token'); 383 384 window.localStorage.clear(); 385 window.localStorage.nomadTokenSecret = managementToken.secretId; 386 }); 387 388 test('search field narrows summary table results, changes the active summary if it no longer matches, and displays a no matches message when there are none', async function(assert) { 389 server.create('job', { 390 name: 'zzzzzz', 391 createRecommendations: true, 392 groupsCount: 1, 393 groupTaskCount: 6, 394 }); 395 396 // Ensure this job’s recommendations are sorted to the top of the table 397 const futureSubmitTime = (Date.now() + 10000) * 1000000; 398 server.db.recommendations.update({ submitTime: futureSubmitTime }); 399 400 server.create('job', { 401 name: 'oooooo', 402 createRecommendations: true, 403 groupsCount: 2, 404 groupTaskCount: 4, 405 }); 406 407 server.create('job', { 408 name: 'pppppp', 409 createRecommendations: true, 410 groupsCount: 2, 411 groupTaskCount: 4, 412 }); 413 414 await Optimize.visit(); 415 416 assert.equal(Optimize.card.slug.jobName, 'zzzzzz'); 417 418 assert.equal( 419 Optimize.search.placeholder, 420 `Search ${Optimize.recommendationSummaries.length} recommendations...` 421 ); 422 423 await Optimize.search.fillIn('ooo'); 424 425 assert.equal(Optimize.recommendationSummaries.length, 2); 426 assert.ok(Optimize.recommendationSummaries[0].slug.startsWith('oooooo')); 427 428 assert.equal(Optimize.card.slug.jobName, 'oooooo'); 429 assert.ok(currentURL().includes('oooooo')); 430 431 await Optimize.search.fillIn('qqq'); 432 433 assert.notOk(Optimize.card.isPresent); 434 assert.ok(Optimize.empty.isPresent); 435 assert.equal(Optimize.empty.headline, 'No Matches'); 436 assert.equal(currentURL(), '/optimize?search=qqq'); 437 438 await Optimize.search.fillIn(''); 439 440 assert.equal(Optimize.card.slug.jobName, 'zzzzzz'); 441 assert.ok(Optimize.recommendationSummaries[0].isActive); 442 }); 443 444 test('turning off the namespaces toggle narrows summaries to only the current namespace and changes an active summary if it has become filtered out', async function(assert) { 445 server.create('job', { 446 name: 'pppppp', 447 createRecommendations: true, 448 groupsCount: 1, 449 groupTaskCount: 4, 450 namespaceId: server.db.namespaces[1].id, 451 }); 452 453 // Ensure this job’s recommendations are sorted to the top of the table 454 const futureSubmitTime = (Date.now() + 10000) * 1000000; 455 server.db.recommendations.update({ submitTime: futureSubmitTime }); 456 457 server.create('job', { 458 name: 'oooooo', 459 createRecommendations: true, 460 groupsCount: 1, 461 groupTaskCount: 6, 462 namespaceId: server.db.namespaces[0].id, 463 }); 464 465 await Optimize.visit(); 466 467 assert.ok(Optimize.allNamespacesToggle.isActive); 468 469 await Optimize.allNamespacesToggle.toggle(); 470 471 assert.equal(Optimize.recommendationSummaries.length, 1); 472 assert.ok(Optimize.recommendationSummaries[0].slug.startsWith('ooo')); 473 assert.ok(currentURL().includes('all-namespaces=false')); 474 assert.equal(Optimize.card.slug.jobName, 'oooooo'); 475 }); 476 477 test('the namespaces toggle doesn’t show when there aren’t namespaces', async function(assert) { 478 server.db.namespaces.remove(); 479 480 server.create('job', { 481 createRecommendations: true, 482 groupsCount: 1, 483 groupTaskCount: 4, 484 }); 485 486 await Optimize.visit(); 487 488 assert.ok(Optimize.allNamespacesToggle.isHidden); 489 }); 490 491 test('processing a summary moves to the next one in the sorted list', async function(assert) { 492 server.create('job', { 493 name: 'ooo111', 494 createRecommendations: true, 495 groupsCount: 1, 496 groupTaskCount: 4, 497 }); 498 499 server.create('job', { 500 name: 'pppppp', 501 createRecommendations: true, 502 groupsCount: 1, 503 groupTaskCount: 4, 504 }); 505 506 server.create('job', { 507 name: 'ooo222', 508 createRecommendations: true, 509 groupsCount: 1, 510 groupTaskCount: 4, 511 }); 512 513 // Directly set the sorting of the above jobs’s summaries in the table 514 const futureSubmitTime = (Date.now() + 10000) * 1000000; 515 const nowSubmitTime = Date.now() * 1000000; 516 const pastSubmitTime = (Date.now() - 10000) * 1000000; 517 518 const jobNameToRecommendationSubmitTime = { 519 ooo111: futureSubmitTime, 520 pppppp: nowSubmitTime, 521 ooo222: pastSubmitTime, 522 }; 523 524 server.schema.recommendations.all().models.forEach(recommendation => { 525 const parentJob = recommendation.task.taskGroup.job; 526 const submitTimeForJob = jobNameToRecommendationSubmitTime[parentJob.name]; 527 recommendation.submitTime = submitTimeForJob; 528 recommendation.save(); 529 }); 530 531 await Optimize.visit(); 532 await Optimize.search.fillIn('ooo'); 533 await Optimize.card.acceptButton.click(); 534 535 assert.equal(Optimize.card.slug.jobName, 'ooo222'); 536 }); 537 538 test('the optimize page has appropriate faceted search options', async function(assert) { 539 server.createList('job', 4, { 540 status: 'running', 541 createRecommendations: true, 542 childrenCount: 0, 543 }); 544 545 await Optimize.visit(); 546 547 assert.ok(Optimize.facets.type.isPresent, 'Type facet found'); 548 assert.ok(Optimize.facets.status.isPresent, 'Status facet found'); 549 assert.ok(Optimize.facets.datacenter.isPresent, 'Datacenter facet found'); 550 assert.ok(Optimize.facets.prefix.isPresent, 'Prefix facet found'); 551 }); 552 553 testFacet('Type', { 554 facet: Optimize.facets.type, 555 paramName: 'type', 556 expectedOptions: ['Service', 'System'], 557 async beforeEach() { 558 server.createList('job', 2, { 559 type: 'service', 560 createRecommendations: true, 561 groupsCount: 1, 562 groupTaskCount: 2, 563 }); 564 565 server.createList('job', 2, { 566 type: 'system', 567 createRecommendations: true, 568 groupsCount: 1, 569 groupTaskCount: 2, 570 }); 571 await Optimize.visit(); 572 }, 573 filter(taskGroup, selection) { 574 let displayType = taskGroup.job.type; 575 return selection.includes(displayType); 576 }, 577 }); 578 579 testFacet('Status', { 580 facet: Optimize.facets.status, 581 paramName: 'status', 582 expectedOptions: ['Pending', 'Running', 'Dead'], 583 async beforeEach() { 584 server.createList('job', 2, { 585 status: 'pending', 586 createRecommendations: true, 587 groupsCount: 1, 588 groupTaskCount: 2, 589 childrenCount: 0, 590 }); 591 server.createList('job', 2, { 592 status: 'running', 593 createRecommendations: true, 594 groupsCount: 1, 595 groupTaskCount: 2, 596 childrenCount: 0, 597 }); 598 server.createList('job', 2, { 599 status: 'dead', 600 createRecommendations: true, 601 childrenCount: 0, 602 }); 603 await Optimize.visit(); 604 }, 605 filter: (taskGroup, selection) => selection.includes(taskGroup.job.status), 606 }); 607 608 testFacet('Datacenter', { 609 facet: Optimize.facets.datacenter, 610 paramName: 'dc', 611 expectedOptions(jobs) { 612 const allDatacenters = new Set( 613 jobs.mapBy('datacenters').reduce((acc, val) => acc.concat(val), []) 614 ); 615 return Array.from(allDatacenters).sort(); 616 }, 617 async beforeEach() { 618 server.create('job', { 619 datacenters: ['pdx', 'lax'], 620 createRecommendations: true, 621 groupsCount: 1, 622 groupTaskCount: 2, 623 childrenCount: 0, 624 }); 625 server.create('job', { 626 datacenters: ['pdx', 'ord'], 627 createRecommendations: true, 628 groupsCount: 1, 629 groupTaskCount: 2, 630 childrenCount: 0, 631 }); 632 server.create('job', { 633 datacenters: ['lax', 'jfk'], 634 createRecommendations: true, 635 groupsCount: 1, 636 groupTaskCount: 2, 637 childrenCount: 0, 638 }); 639 server.create('job', { 640 datacenters: ['jfk', 'dfw'], 641 createRecommendations: true, 642 groupsCount: 1, 643 groupTaskCount: 2, 644 childrenCount: 0, 645 }); 646 server.create('job', { datacenters: ['pdx'], createRecommendations: true, childrenCount: 0 }); 647 await Optimize.visit(); 648 }, 649 filter: (taskGroup, selection) => taskGroup.job.datacenters.find(dc => selection.includes(dc)), 650 }); 651 652 testFacet('Prefix', { 653 facet: Optimize.facets.prefix, 654 paramName: 'prefix', 655 expectedOptions: ['hashi (3)', 'nmd (2)', 'pre (2)'], 656 async beforeEach() { 657 [ 658 'pre-one', 659 'hashi_one', 660 'nmd.one', 661 'one-alone', 662 'pre_two', 663 'hashi.two', 664 'hashi-three', 665 'nmd_two', 666 'noprefix', 667 ].forEach(name => { 668 server.create('job', { 669 name, 670 createRecommendations: true, 671 createAllocations: true, 672 groupsCount: 1, 673 groupTaskCount: 2, 674 childrenCount: 0, 675 }); 676 }); 677 await Optimize.visit(); 678 }, 679 filter: (taskGroup, selection) => 680 selection.find(prefix => taskGroup.job.name.startsWith(prefix)), 681 }); 682 683 function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) { 684 test(`the ${label} facet has the correct options`, async function(assert) { 685 await beforeEach(); 686 await facet.toggle(); 687 688 let expectation; 689 if (typeof expectedOptions === 'function') { 690 expectation = expectedOptions(server.db.jobs); 691 } else { 692 expectation = expectedOptions; 693 } 694 695 assert.deepEqual( 696 facet.options.map(option => option.label.trim()), 697 expectation, 698 'Options for facet are as expected' 699 ); 700 }); 701 702 test(`the ${label} facet filters the recommendation summaries by ${label}`, async function(assert) { 703 let option; 704 705 await beforeEach(); 706 await facet.toggle(); 707 708 option = facet.options.objectAt(0); 709 await option.toggle(); 710 711 const selection = [option.key]; 712 713 const sortedRecommendations = server.db.recommendations.sortBy('submitTime').reverse(); 714 715 const recommendationTaskGroups = server.schema.tasks 716 .find(sortedRecommendations.mapBy('taskId').uniq()) 717 .models.mapBy('taskGroup') 718 .uniqBy('id') 719 .filter(group => filter(group, selection)); 720 721 Optimize.recommendationSummaries.forEach((summary, index) => { 722 const group = recommendationTaskGroups[index]; 723 assert.equal(summary.slug, `${group.job.name} / ${group.name}`); 724 }); 725 }); 726 727 test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) { 728 const selection = []; 729 730 await beforeEach(); 731 await facet.toggle(); 732 733 const option1 = facet.options.objectAt(0); 734 const option2 = facet.options.objectAt(1); 735 await option1.toggle(); 736 selection.push(option1.key); 737 await option2.toggle(); 738 selection.push(option2.key); 739 740 const sortedRecommendations = server.db.recommendations.sortBy('submitTime').reverse(); 741 742 const recommendationTaskGroups = server.schema.tasks 743 .find(sortedRecommendations.mapBy('taskId').uniq()) 744 .models.mapBy('taskGroup') 745 .uniqBy('id') 746 .filter(group => filter(group, selection)); 747 748 Optimize.recommendationSummaries.forEach((summary, index) => { 749 const group = recommendationTaskGroups[index]; 750 assert.equal(summary.slug, `${group.job.name} / ${group.name}`); 751 }); 752 }); 753 754 test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) { 755 const selection = []; 756 757 await beforeEach(); 758 await facet.toggle(); 759 760 const option1 = facet.options.objectAt(0); 761 const option2 = facet.options.objectAt(1); 762 await option1.toggle(); 763 selection.push(option1.key); 764 await option2.toggle(); 765 selection.push(option2.key); 766 767 assert.ok(currentURL().includes(encodeURIComponent(JSON.stringify(selection)))); 768 }); 769 } 770 }); 771 772 function formattedMemDiff(memDiff) { 773 const absMemDiff = Math.abs(memDiff); 774 const negativeSign = memDiff < 0 ? '-' : ''; 775 776 if (absMemDiff >= 1024) { 777 const gibDiff = absMemDiff / 1024; 778 779 if (Number.isInteger(gibDiff)) { 780 return `${negativeSign}${gibDiff} GiB`; 781 } else { 782 return `${negativeSign}${gibDiff.toFixed(2)} GiB`; 783 } 784 } else { 785 return `${negativeSign}${absMemDiff} MiB`; 786 } 787 }