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