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