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