github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/task-group-detail-test.js (about) 1 /* eslint-disable qunit/require-expect */ 2 /* eslint-disable qunit/no-conditional-assertions */ 3 import { currentURL, settled } from '@ember/test-helpers'; 4 import { module, test } from 'qunit'; 5 import { setupApplicationTest } from 'ember-qunit'; 6 import { setupMirage } from 'ember-cli-mirage/test-support'; 7 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 8 import { 9 formatBytes, 10 formatHertz, 11 formatScheduledBytes, 12 formatScheduledHertz, 13 } from 'nomad-ui/utils/units'; 14 import TaskGroup from 'nomad-ui/tests/pages/jobs/job/task-group'; 15 import Layout from 'nomad-ui/tests/pages/layout'; 16 import pageSizeSelect from './behaviors/page-size-select'; 17 import moment from 'moment'; 18 19 let job; 20 let taskGroup; 21 let tasks; 22 let allocations; 23 let managementToken; 24 25 const sum = (total, n) => total + n; 26 27 module('Acceptance | task group detail', function (hooks) { 28 setupApplicationTest(hooks); 29 setupMirage(hooks); 30 31 hooks.beforeEach(async function () { 32 server.create('agent'); 33 server.create('node', 'forceIPv4'); 34 35 job = server.create('job', { 36 groupsCount: 2, 37 createAllocations: false, 38 }); 39 40 const taskGroups = server.db.taskGroups.where({ jobId: job.id }); 41 taskGroup = taskGroups[0]; 42 43 tasks = taskGroup.taskIds.map((id) => server.db.tasks.find(id)); 44 45 server.create('node', 'forceIPv4'); 46 47 allocations = server.createList('allocation', 2, { 48 jobId: job.id, 49 taskGroup: taskGroup.name, 50 clientStatus: 'running', 51 }); 52 53 // Allocations associated to a different task group on the job to 54 // assert that they aren't showing up in on this page in error. 55 server.createList('allocation', 3, { 56 jobId: job.id, 57 taskGroup: taskGroups[1].name, 58 clientStatus: 'running', 59 }); 60 61 // Set a static name to make the search test deterministic 62 server.db.allocations.forEach((alloc) => { 63 alloc.name = 'aaaaa'; 64 }); 65 66 // Mark the first alloc as rescheduled 67 allocations[0].update({ 68 nextAllocation: allocations[1].id, 69 }); 70 allocations[1].update({ 71 previousAllocation: allocations[0].id, 72 }); 73 74 managementToken = server.create('token'); 75 76 window.localStorage.clear(); 77 }); 78 79 test('it passes an accessibility audit', async function (assert) { 80 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 81 await a11yAudit(assert); 82 }); 83 84 test('/jobs/:id/:task-group should list high-level metrics for the allocation', async function (assert) { 85 const totalCPU = tasks.mapBy('resources.CPU').reduce(sum, 0); 86 const totalMemory = tasks.mapBy('resources.MemoryMB').reduce(sum, 0); 87 const totalMemoryMax = tasks 88 .map((t) => t.resources.MemoryMaxMB || t.resources.MemoryMB) 89 .reduce(sum, 0); 90 const totalDisk = taskGroup.ephemeralDisk.SizeMB; 91 92 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 93 94 assert.equal(TaskGroup.tasksCount, `# Tasks ${tasks.length}`, '# Tasks'); 95 assert.equal( 96 TaskGroup.cpu, 97 `Reserved CPU ${formatScheduledHertz(totalCPU, 'MHz')}`, 98 'Aggregated CPU reservation for all tasks' 99 ); 100 101 let totalMemoryMaxAddendum = ''; 102 103 if (totalMemoryMax > totalMemory) { 104 totalMemoryMaxAddendum = ` (${formatScheduledBytes( 105 totalMemoryMax, 106 'MiB' 107 )}Max)`; 108 } 109 110 assert.equal( 111 TaskGroup.mem, 112 `Reserved Memory ${formatScheduledBytes( 113 totalMemory, 114 'MiB' 115 )}${totalMemoryMaxAddendum}`, 116 'Aggregated Memory reservation for all tasks' 117 ); 118 assert.equal( 119 TaskGroup.disk, 120 `Reserved Disk ${formatScheduledBytes(totalDisk, 'MiB')}`, 121 'Aggregated Disk reservation for all tasks' 122 ); 123 124 assert.equal( 125 document.title, 126 `Task group ${taskGroup.name} - Job ${job.name} - Nomad` 127 ); 128 }); 129 130 test('/jobs/:id/:task-group should have breadcrumbs for job and jobs', async function (assert) { 131 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 132 133 assert.equal( 134 Layout.breadcrumbFor('jobs.index').text, 135 'Jobs', 136 'First breadcrumb says jobs' 137 ); 138 assert.equal( 139 Layout.breadcrumbFor('jobs.job.index').text, 140 `Job ${job.name}`, 141 'Second breadcrumb says the job name' 142 ); 143 assert.equal( 144 Layout.breadcrumbFor('jobs.job.task-group').text, 145 `Task Group ${taskGroup.name}`, 146 'Third breadcrumb says the job name' 147 ); 148 }); 149 150 test('/jobs/:id/:task-group first breadcrumb should link to jobs', async function (assert) { 151 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 152 153 await Layout.breadcrumbFor('jobs.index').visit(); 154 assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); 155 }); 156 157 test('/jobs/:id/:task-group second breadcrumb should link to the job for the task group', async function (assert) { 158 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 159 160 await Layout.breadcrumbFor('jobs.job.index').visit(); 161 assert.equal( 162 currentURL(), 163 `/jobs/${job.id}`, 164 'Second breadcrumb links back to the job for the task group' 165 ); 166 }); 167 168 test('when the user has a client token that has a namespace with a policy to run and scale a job the autoscaler options should be available', async function (assert) { 169 window.localStorage.clear(); 170 171 const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace'; 172 const READ_ONLY_NAMESPACE = 'read-only-namespace'; 173 const clientToken = server.create('token'); 174 175 server.create('namespace', { id: SCALE_AND_WRITE_NAMESPACE }); 176 const secondNamespace = server.create('namespace', { 177 id: READ_ONLY_NAMESPACE, 178 }); 179 180 job = server.create('job', { 181 groupCount: 0, 182 createAllocations: false, 183 shallow: true, 184 noActiveDeployment: true, 185 namespaceId: SCALE_AND_WRITE_NAMESPACE, 186 }); 187 const scalingGroup = server.create('task-group', { 188 job, 189 name: 'scaling', 190 count: 1, 191 shallow: true, 192 withScaling: true, 193 }); 194 job.update({ taskGroupIds: [scalingGroup.id] }); 195 196 const job2 = server.create('job', { 197 groupCount: 0, 198 createAllocations: false, 199 shallow: true, 200 noActiveDeployment: true, 201 namespaceId: READ_ONLY_NAMESPACE, 202 }); 203 const scalingGroup2 = server.create('task-group', { 204 job: job2, 205 name: 'scaling', 206 count: 1, 207 shallow: true, 208 withScaling: true, 209 }); 210 job2.update({ taskGroupIds: [scalingGroup2.id] }); 211 212 const policy = server.create('policy', { 213 id: 'something', 214 name: 'something', 215 rulesJSON: { 216 Namespaces: [ 217 { 218 Name: SCALE_AND_WRITE_NAMESPACE, 219 Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'], 220 }, 221 { 222 Name: READ_ONLY_NAMESPACE, 223 Capabilities: ['list-jobs', 'read-job'], 224 }, 225 ], 226 }, 227 }); 228 229 clientToken.policyIds = [policy.id]; 230 clientToken.save(); 231 232 window.localStorage.nomadTokenSecret = clientToken.secretId; 233 234 await TaskGroup.visit({ 235 id: `${job.id}@${SCALE_AND_WRITE_NAMESPACE}`, 236 name: scalingGroup.name, 237 }); 238 239 assert.equal( 240 decodeURIComponent(currentURL()), 241 `/jobs/${job.id}@${SCALE_AND_WRITE_NAMESPACE}/scaling` 242 ); 243 assert.notOk(TaskGroup.countStepper.increment.isDisabled); 244 245 await TaskGroup.visit({ 246 id: `${job2.id}@${secondNamespace.name}`, 247 name: scalingGroup2.name, 248 }); 249 assert.equal( 250 decodeURIComponent(currentURL()), 251 `/jobs/${job2.id}@${READ_ONLY_NAMESPACE}/scaling` 252 ); 253 assert.ok(TaskGroup.countStepper.increment.isDisabled); 254 }); 255 256 test('/jobs/:id/:task-group should list one page of allocations for the task group', async function (assert) { 257 server.createList('allocation', TaskGroup.pageSize, { 258 jobId: job.id, 259 taskGroup: taskGroup.name, 260 clientStatus: 'running', 261 }); 262 263 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 264 265 assert.ok( 266 server.db.allocations.where({ jobId: job.id }).length > 267 TaskGroup.pageSize, 268 'There are enough allocations to invoke pagination' 269 ); 270 271 assert.equal( 272 TaskGroup.allocations.length, 273 TaskGroup.pageSize, 274 'All allocations for the task group' 275 ); 276 }); 277 278 test('each allocation should show basic information about the allocation', async function (assert) { 279 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 280 281 const allocation = allocations.sortBy('modifyIndex').reverse()[0]; 282 const allocationRow = TaskGroup.allocations.objectAt(0); 283 284 assert.equal( 285 allocationRow.shortId, 286 allocation.id.split('-')[0], 287 'Allocation short id' 288 ); 289 assert.equal( 290 allocationRow.createTime, 291 moment(allocation.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), 292 'Allocation create time' 293 ); 294 assert.equal( 295 allocationRow.modifyTime, 296 moment(allocation.modifyTime / 1000000).fromNow(), 297 'Allocation modify time' 298 ); 299 assert.equal( 300 allocationRow.status, 301 allocation.clientStatus, 302 'Client status' 303 ); 304 assert.equal( 305 allocationRow.jobVersion, 306 allocation.jobVersion, 307 'Job Version' 308 ); 309 assert.equal( 310 allocationRow.client, 311 server.db.nodes.find(allocation.nodeId).id.split('-')[0], 312 'Node ID' 313 ); 314 assert.equal( 315 allocationRow.volume, 316 Object.keys(taskGroup.volumes).length ? 'Yes' : '', 317 'Volumes' 318 ); 319 320 await allocationRow.visitClient(); 321 322 assert.equal( 323 currentURL(), 324 `/clients/${allocation.nodeId}`, 325 'Node links to node page' 326 ); 327 }); 328 329 test('each allocation should show stats about the allocation', async function (assert) { 330 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 331 332 const allocation = allocations.sortBy('name')[0]; 333 const allocationRow = TaskGroup.allocations.objectAt(0); 334 335 const allocStats = server.db.clientAllocationStats.find(allocation.id); 336 const tasks = taskGroup.taskIds.map((id) => server.db.tasks.find(id)); 337 338 const cpuUsed = tasks.reduce((sum, task) => sum + task.resources.CPU, 0); 339 const memoryUsed = tasks.reduce( 340 (sum, task) => sum + task.resources.MemoryMB, 341 0 342 ); 343 344 assert.equal( 345 allocationRow.cpu, 346 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 347 'CPU %' 348 ); 349 350 const roundedTicks = Math.floor( 351 allocStats.resourceUsage.CpuStats.TotalTicks 352 ); 353 assert.equal( 354 allocationRow.cpuTooltip, 355 `${formatHertz(roundedTicks, 'MHz')} / ${formatHertz(cpuUsed, 'MHz')}`, 356 'Detailed CPU information is in a tooltip' 357 ); 358 359 assert.equal( 360 allocationRow.mem, 361 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 362 'Memory used' 363 ); 364 365 assert.equal( 366 allocationRow.memTooltip, 367 `${formatBytes(allocStats.resourceUsage.MemoryStats.RSS)} / ${formatBytes( 368 memoryUsed, 369 'MiB' 370 )}`, 371 'Detailed memory information is in a tooltip' 372 ); 373 }); 374 375 test('when the allocation search has no matches, there is an empty message', async function (assert) { 376 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 377 378 await TaskGroup.search('zzzzzz'); 379 380 assert.ok(TaskGroup.isEmpty, 'Empty state is shown'); 381 assert.equal( 382 TaskGroup.emptyState.headline, 383 'No Matches', 384 'Empty state has an appropriate message' 385 ); 386 }); 387 388 test('when the allocation has reschedule events, the allocation row is denoted with an icon', async function (assert) { 389 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 390 391 const rescheduleRow = TaskGroup.allocationFor(allocations[0].id); 392 const normalRow = TaskGroup.allocationFor(allocations[1].id); 393 394 assert.ok( 395 rescheduleRow.rescheduled, 396 'Reschedule row has a reschedule icon' 397 ); 398 assert.notOk(normalRow.rescheduled, 'Normal row has no reschedule icon'); 399 }); 400 401 test('/jobs/:id/:task-group should present task lifecycles', async function (assert) { 402 job = server.create('job', { 403 groupsCount: 2, 404 groupTaskCount: 3, 405 }); 406 407 const taskGroups = server.db.taskGroups.where({ jobId: job.id }); 408 taskGroup = taskGroups[0]; 409 410 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 411 412 assert.ok(TaskGroup.lifecycleChart.isPresent); 413 assert.equal( 414 TaskGroup.lifecycleChart.title, 415 'Task Lifecycle Configuration' 416 ); 417 418 tasks = taskGroup.taskIds.map((id) => server.db.tasks.find(id)); 419 const taskNames = tasks.mapBy('name'); 420 421 // This is thoroughly tested in allocation detail tests, so this mostly checks what’s different 422 423 assert.equal(TaskGroup.lifecycleChart.tasks.length, 3); 424 425 TaskGroup.lifecycleChart.tasks.forEach((Task) => { 426 assert.ok(taskNames.includes(Task.name)); 427 assert.notOk(Task.isActive); 428 assert.notOk(Task.isFinished); 429 }); 430 }); 431 432 test('when the task group depends on volumes, the volumes table is shown', async function (assert) { 433 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 434 435 assert.ok(TaskGroup.hasVolumes); 436 assert.equal( 437 TaskGroup.volumes.length, 438 Object.keys(taskGroup.volumes).length 439 ); 440 }); 441 442 test('when the task group does not depend on volumes, the volumes table is not shown', async function (assert) { 443 job = server.create('job', { noHostVolumes: true, shallow: true }); 444 taskGroup = server.db.taskGroups.where({ jobId: job.id })[0]; 445 446 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 447 448 assert.notOk(TaskGroup.hasVolumes); 449 }); 450 451 test('each row in the volumes table lists information about the volume', async function (assert) { 452 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 453 454 TaskGroup.volumes[0].as((volumeRow) => { 455 const volume = taskGroup.volumes[volumeRow.name]; 456 assert.equal(volumeRow.name, volume.Name); 457 assert.equal(volumeRow.type, volume.Type); 458 assert.equal(volumeRow.source, volume.Source); 459 assert.equal( 460 volumeRow.permissions, 461 volume.ReadOnly ? 'Read' : 'Read/Write' 462 ); 463 }); 464 }); 465 466 test('the count stepper sends the appropriate POST request', async function (assert) { 467 window.localStorage.nomadTokenSecret = managementToken.secretId; 468 469 job = server.create('job', { 470 groupCount: 0, 471 createAllocations: false, 472 shallow: true, 473 noActiveDeployment: true, 474 }); 475 const scalingGroup = server.create('task-group', { 476 job, 477 name: 'scaling', 478 count: 1, 479 shallow: true, 480 withScaling: true, 481 }); 482 job.update({ taskGroupIds: [scalingGroup.id] }); 483 484 await TaskGroup.visit({ id: job.id, name: scalingGroup.name }); 485 await TaskGroup.countStepper.increment.click(); 486 await settled(); 487 488 const scaleRequest = server.pretender.handledRequests.find( 489 (req) => req.method === 'POST' && req.url.endsWith('/scale') 490 ); 491 const requestBody = JSON.parse(scaleRequest.requestBody); 492 assert.equal(requestBody.Target.Group, scalingGroup.name); 493 assert.equal(requestBody.Count, scalingGroup.count + 1); 494 }); 495 496 test('the count stepper is disabled when a deployment is running', async function (assert) { 497 window.localStorage.nomadTokenSecret = managementToken.secretId; 498 499 job = server.create('job', { 500 groupCount: 0, 501 createAllocations: false, 502 shallow: true, 503 activeDeployment: true, 504 }); 505 const scalingGroup = server.create('task-group', { 506 job, 507 name: 'scaling', 508 count: 1, 509 shallow: true, 510 withScaling: true, 511 }); 512 job.update({ taskGroupIds: [scalingGroup.id] }); 513 514 await TaskGroup.visit({ id: job.id, name: scalingGroup.name }); 515 516 assert.ok(TaskGroup.countStepper.input.isDisabled); 517 assert.ok(TaskGroup.countStepper.increment.isDisabled); 518 assert.ok(TaskGroup.countStepper.decrement.isDisabled); 519 }); 520 521 test('when the job for the task group is not found, an error message is shown, but the URL persists', async function (assert) { 522 await TaskGroup.visit({ 523 id: 'not-a-real-job', 524 name: 'not-a-real-task-group', 525 }); 526 527 assert.equal( 528 server.pretender.handledRequests 529 .filter((request) => !request.url.includes('policy')) 530 .findBy('status', 404).url, 531 '/v1/job/not-a-real-job', 532 'A request to the nonexistent job is made' 533 ); 534 assert.equal( 535 currentURL(), 536 '/jobs/not-a-real-job/not-a-real-task-group', 537 'The URL persists' 538 ); 539 assert.ok(TaskGroup.error.isPresent, 'Error message is shown'); 540 assert.equal( 541 TaskGroup.error.title, 542 'Not Found', 543 'Error message is for 404' 544 ); 545 }); 546 547 test('when the task group is not found on the job, an error message is shown, but the URL persists', async function (assert) { 548 await TaskGroup.visit({ id: job.id, name: 'not-a-real-task-group' }); 549 550 assert.ok( 551 server.pretender.handledRequests 552 .filterBy('status', 200) 553 .mapBy('url') 554 .includes(`/v1/job/${job.id}`), 555 'A request to the job is made and succeeds' 556 ); 557 assert.equal( 558 currentURL(), 559 `/jobs/${job.id}/not-a-real-task-group`, 560 'The URL persists' 561 ); 562 assert.ok(TaskGroup.error.isPresent, 'Error message is shown'); 563 assert.equal( 564 TaskGroup.error.title, 565 'Not Found', 566 'Error message is for 404' 567 ); 568 }); 569 570 pageSizeSelect({ 571 resourceName: 'allocation', 572 pageObject: TaskGroup, 573 pageObjectList: TaskGroup.allocations, 574 async setup() { 575 server.createList('allocation', TaskGroup.pageSize, { 576 jobId: job.id, 577 taskGroup: taskGroup.name, 578 clientStatus: 'running', 579 }); 580 581 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 582 }, 583 }); 584 585 test('when a task group has no scaling events, there is no recent scaling events section', async function (assert) { 586 const taskGroupScale = job.jobScale.taskGroupScales.models.find( 587 (m) => m.name === taskGroup.name 588 ); 589 taskGroupScale.update({ events: [] }); 590 591 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 592 593 assert.notOk(TaskGroup.hasScaleEvents); 594 }); 595 596 test('the recent scaling events section shows all recent scaling events in reverse chronological order', async function (assert) { 597 const taskGroupScale = job.jobScale.taskGroupScales.models.find( 598 (m) => m.name === taskGroup.name 599 ); 600 taskGroupScale.update({ 601 events: [ 602 server.create('scale-event', { error: true }), 603 server.create('scale-event', { error: true }), 604 server.create('scale-event', { error: true }), 605 server.create('scale-event', { error: true }), 606 server.create('scale-event', { count: 3, error: false }), 607 server.create('scale-event', { count: 1, error: false }), 608 ], 609 }); 610 const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse(); 611 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 612 613 assert.ok(TaskGroup.hasScaleEvents); 614 assert.notOk(TaskGroup.hasScalingTimeline); 615 616 scaleEvents.forEach((scaleEvent, idx) => { 617 const ScaleEvent = TaskGroup.scaleEvents[idx]; 618 assert.equal( 619 ScaleEvent.time, 620 moment(scaleEvent.time / 1000000).format('MMM DD HH:mm:ss ZZ') 621 ); 622 assert.equal(ScaleEvent.message, scaleEvent.message); 623 624 if (scaleEvent.count != null) { 625 assert.equal(ScaleEvent.count, scaleEvent.count); 626 } 627 628 if (scaleEvent.error) { 629 assert.ok(ScaleEvent.error); 630 } 631 632 if (Object.keys(scaleEvent.meta).length) { 633 assert.ok(ScaleEvent.isToggleable); 634 } else { 635 assert.notOk(ScaleEvent.isToggleable); 636 } 637 }); 638 }); 639 640 test('when a task group has at least two count scaling events and the count scaling events outnumber the non-count scaling events, a timeline is shown in addition to the accordion', async function (assert) { 641 const taskGroupScale = job.jobScale.taskGroupScales.models.find( 642 (m) => m.name === taskGroup.name 643 ); 644 taskGroupScale.update({ 645 events: [ 646 server.create('scale-event', { error: true }), 647 server.create('scale-event', { error: true }), 648 server.create('scale-event', { count: 7, error: false }), 649 server.create('scale-event', { count: 10, error: false }), 650 server.create('scale-event', { count: 2, error: false }), 651 server.create('scale-event', { count: 3, error: false }), 652 server.create('scale-event', { count: 2, error: false }), 653 server.create('scale-event', { count: 9, error: false }), 654 server.create('scale-event', { count: 1, error: false }), 655 ], 656 }); 657 const scaleEvents = taskGroupScale.events.models.sortBy('time').reverse(); 658 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 659 660 assert.ok(TaskGroup.hasScaleEvents); 661 assert.ok(TaskGroup.hasScalingTimeline); 662 663 assert.equal( 664 TaskGroup.scalingAnnotations.length, 665 scaleEvents.filter((ev) => ev.count == null).length 666 ); 667 }); 668 669 testFacet('Status', { 670 facet: TaskGroup.facets.status, 671 paramName: 'status', 672 expectedOptions: [ 673 'Pending', 674 'Running', 675 'Complete', 676 'Failed', 677 'Lost', 678 'Unknown', 679 ], 680 async beforeEach() { 681 ['pending', 'running', 'complete', 'failed', 'lost', 'unknown'].forEach( 682 (s) => { 683 server.createList('allocation', 5, { clientStatus: s }); 684 } 685 ); 686 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 687 }, 688 filter: (alloc, selection) => 689 alloc.jobId == job.id && 690 alloc.taskGroup == taskGroup.name && 691 selection.includes(alloc.clientStatus), 692 }); 693 694 testFacet('Client', { 695 facet: TaskGroup.facets.client, 696 paramName: 'client', 697 expectedOptions(allocs) { 698 return Array.from( 699 new Set( 700 allocs 701 .filter( 702 (alloc) => 703 alloc.jobId == job.id && alloc.taskGroup == taskGroup.name 704 ) 705 .mapBy('nodeId') 706 .map((id) => id.split('-')[0]) 707 ) 708 ).sort(); 709 }, 710 async beforeEach() { 711 const nodes = server.createList('node', 3, 'forceIPv4'); 712 nodes.forEach((node) => 713 server.createList('allocation', 5, { 714 nodeId: node.id, 715 jobId: job.id, 716 taskGroup: taskGroup.name, 717 }) 718 ); 719 await TaskGroup.visit({ id: job.id, name: taskGroup.name }); 720 }, 721 filter: (alloc, selection) => 722 alloc.jobId == job.id && 723 alloc.taskGroup == taskGroup.name && 724 selection.includes(alloc.nodeId.split('-')[0]), 725 }); 726 }); 727 728 function testFacet( 729 label, 730 { facet, paramName, beforeEach, filter, expectedOptions } 731 ) { 732 test(`facet ${label} | the ${label} facet has the correct options`, async function (assert) { 733 await beforeEach(); 734 await facet.toggle(); 735 736 let expectation; 737 if (typeof expectedOptions === 'function') { 738 expectation = expectedOptions(server.db.allocations); 739 } else { 740 expectation = expectedOptions; 741 } 742 743 assert.deepEqual( 744 facet.options.map((option) => option.label.trim()), 745 expectation, 746 'Options for facet are as expected' 747 ); 748 }); 749 750 test(`facet ${label} | the ${label} facet filters the allocations list by ${label}`, async function (assert) { 751 let option; 752 753 await beforeEach(); 754 755 await facet.toggle(); 756 option = facet.options.objectAt(0); 757 await option.toggle(); 758 759 const selection = [option.key]; 760 const expectedAllocs = server.db.allocations 761 .filter((alloc) => filter(alloc, selection)) 762 .sortBy('modifyIndex') 763 .reverse(); 764 765 TaskGroup.allocations.forEach((alloc, index) => { 766 assert.equal( 767 alloc.id, 768 expectedAllocs[index].id, 769 `Allocation at ${index} is ${expectedAllocs[index].id}` 770 ); 771 }); 772 }); 773 774 test(`facet ${label} | selecting multiple options in the ${label} facet results in a broader search`, async function (assert) { 775 const selection = []; 776 777 await beforeEach(); 778 await facet.toggle(); 779 780 const option1 = facet.options.objectAt(0); 781 const option2 = facet.options.objectAt(1); 782 await option1.toggle(); 783 selection.push(option1.key); 784 await option2.toggle(); 785 selection.push(option2.key); 786 787 const expectedAllocs = server.db.allocations 788 .filter((alloc) => filter(alloc, selection)) 789 .sortBy('modifyIndex') 790 .reverse(); 791 792 TaskGroup.allocations.forEach((alloc, index) => { 793 assert.equal( 794 alloc.id, 795 expectedAllocs[index].id, 796 `Allocation at ${index} is ${expectedAllocs[index].id}` 797 ); 798 }); 799 }); 800 801 test(`facet ${label} | selecting options in the ${label} facet updates the ${paramName} query param`, async function (assert) { 802 const selection = []; 803 804 await beforeEach(); 805 await facet.toggle(); 806 807 const option1 = facet.options.objectAt(0); 808 const option2 = facet.options.objectAt(1); 809 await option1.toggle(); 810 selection.push(option1.key); 811 await option2.toggle(); 812 selection.push(option2.key); 813 814 assert.equal( 815 currentURL(), 816 `/jobs/${job.id}/${taskGroup.name}?${paramName}=${encodeURIComponent( 817 JSON.stringify(selection) 818 )}`, 819 'URL has the correct query param key and value' 820 ); 821 }); 822 }