github.com/anuvu/nomad@v0.8.7-atom1/ui/tests/acceptance/client-detail-test.js (about) 1 import { assign } from '@ember/polyfills'; 2 import $ from 'jquery'; 3 import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helpers'; 4 import { test } from 'qunit'; 5 import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; 6 import { formatBytes } from 'nomad-ui/helpers/format-bytes'; 7 import formatDuration from 'nomad-ui/utils/format-duration'; 8 import moment from 'moment'; 9 10 let node; 11 12 moduleForAcceptance('Acceptance | client detail', { 13 beforeEach() { 14 server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); 15 node = server.db.nodes[0]; 16 17 // Related models 18 server.create('agent'); 19 server.create('job', { createAllocations: false }); 20 server.createList('allocation', 3, { nodeId: node.id }); 21 }, 22 }); 23 24 test('/clients/:id should have a breadcrumb trail linking back to clients', function(assert) { 25 visit(`/clients/${node.id}`); 26 27 andThen(() => { 28 assert.equal( 29 find('[data-test-breadcrumb="clients"]').textContent.trim(), 30 'Clients', 31 'First breadcrumb says clients' 32 ); 33 assert.equal( 34 find('[data-test-breadcrumb="client"]').textContent.trim(), 35 node.id.split('-')[0], 36 'Second breadcrumb says the node short id' 37 ); 38 }); 39 40 andThen(() => { 41 click(find('[data-test-breadcrumb="clients"]')); 42 }); 43 44 andThen(() => { 45 assert.equal(currentURL(), '/clients', 'First breadcrumb links back to clients'); 46 }); 47 }); 48 49 test('/clients/:id should list immediate details for the node in the title', function(assert) { 50 visit(`/clients/${node.id}`); 51 52 andThen(() => { 53 assert.ok(find('[data-test-title]').textContent.includes(node.name), 'Title includes name'); 54 assert.ok(find('[data-test-title]').textContent.includes(node.id), 'Title includes id'); 55 assert.ok(find(`[data-test-node-status="${node.status}"]`), 'Title includes status light'); 56 }); 57 }); 58 59 test('/clients/:id should list additional detail for the node below the title', function(assert) { 60 visit(`/clients/${node.id}`); 61 62 andThen(() => { 63 assert.ok( 64 find('.inline-definitions .pair') 65 .textContent.trim() 66 .includes(node.status), 67 'Status is in additional details' 68 ); 69 assert.ok( 70 $('[data-test-status-definition] .status-text').hasClass(`node-${node.status}`), 71 'Status is decorated with a status class' 72 ); 73 assert.ok( 74 find('[data-test-address-definition]') 75 .textContent.trim() 76 .includes(node.httpAddr), 77 'Address is in additional details' 78 ); 79 assert.ok( 80 find('[data-test-draining]') 81 .textContent.trim() 82 .includes(node.drain + ''), 83 'Drain status is in additional details' 84 ); 85 assert.ok( 86 find('[data-test-eligibility]') 87 .textContent.trim() 88 .includes(node.schedulingEligibility), 89 'Scheduling eligibility is in additional details' 90 ); 91 assert.ok( 92 find('[data-test-datacenter-definition]') 93 .textContent.trim() 94 .includes(node.datacenter), 95 'Datacenter is in additional details' 96 ); 97 }); 98 }); 99 100 test('/clients/:id should list all allocations on the node', function(assert) { 101 const allocationsCount = server.db.allocations.where({ nodeId: node.id }).length; 102 103 visit(`/clients/${node.id}`); 104 105 andThen(() => { 106 assert.equal( 107 findAll('[data-test-allocation]').length, 108 allocationsCount, 109 `Allocations table lists all ${allocationsCount} associated allocations` 110 ); 111 }); 112 }); 113 114 test('each allocation should have high-level details for the allocation', function(assert) { 115 const allocation = server.db.allocations 116 .where({ nodeId: node.id }) 117 .sortBy('modifyIndex') 118 .reverse()[0]; 119 120 const allocStats = server.db.clientAllocationStats.find(allocation.id); 121 const taskGroup = server.db.taskGroups.findBy({ 122 name: allocation.taskGroup, 123 jobId: allocation.jobId, 124 }); 125 126 const tasks = taskGroup.taskIds.map(id => server.db.tasks.find(id)); 127 const cpuUsed = tasks.reduce((sum, task) => sum + task.Resources.CPU, 0); 128 const memoryUsed = tasks.reduce((sum, task) => sum + task.Resources.MemoryMB, 0); 129 130 visit(`/clients/${node.id}`); 131 132 andThen(() => { 133 const allocationRow = $(find('[data-test-allocation]')); 134 assert.equal( 135 allocationRow 136 .find('[data-test-short-id]') 137 .text() 138 .trim(), 139 allocation.id.split('-')[0], 140 'Allocation short ID' 141 ); 142 assert.equal( 143 allocationRow 144 .find('[data-test-modify-time]') 145 .text() 146 .trim(), 147 moment(allocation.modifyTime / 1000000).format('MM/DD HH:mm:ss'), 148 'Allocation modify time' 149 ); 150 assert.equal( 151 allocationRow 152 .find('[data-test-name]') 153 .text() 154 .trim(), 155 allocation.name, 156 'Allocation name' 157 ); 158 assert.equal( 159 allocationRow 160 .find('[data-test-client-status]') 161 .text() 162 .trim(), 163 allocation.clientStatus, 164 'Client status' 165 ); 166 assert.equal( 167 allocationRow 168 .find('[data-test-job]') 169 .text() 170 .trim(), 171 server.db.jobs.find(allocation.jobId).name, 172 'Job name' 173 ); 174 assert.ok( 175 allocationRow 176 .find('[data-test-task-group]') 177 .text() 178 .includes(allocation.taskGroup), 179 'Task group name' 180 ); 181 assert.ok( 182 allocationRow 183 .find('[data-test-job-version]') 184 .text() 185 .includes(allocation.jobVersion), 186 'Job Version' 187 ); 188 assert.equal( 189 allocationRow 190 .find('[data-test-cpu]') 191 .text() 192 .trim(), 193 Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks) / cpuUsed, 194 'CPU %' 195 ); 196 assert.equal( 197 allocationRow.find('[data-test-cpu] .tooltip').attr('aria-label'), 198 `${Math.floor(allocStats.resourceUsage.CpuStats.TotalTicks)} / ${cpuUsed} MHz`, 199 'Detailed CPU information is in a tooltip' 200 ); 201 assert.equal( 202 allocationRow 203 .find('[data-test-mem]') 204 .text() 205 .trim(), 206 allocStats.resourceUsage.MemoryStats.RSS / 1024 / 1024 / memoryUsed, 207 'Memory used' 208 ); 209 assert.equal( 210 allocationRow.find('[data-test-mem] .tooltip').attr('aria-label'), 211 `${formatBytes([allocStats.resourceUsage.MemoryStats.RSS])} / ${memoryUsed} MiB`, 212 'Detailed memory information is in a tooltip' 213 ); 214 }); 215 }); 216 217 test('each allocation should show job information even if the job is incomplete and already in the store', function(assert) { 218 // First, visit clients to load the allocations for each visible node. 219 // Don't load the job belongsTo of the allocation! Leave it unfulfilled. 220 221 visit('/clients'); 222 223 // Then, visit jobs to load all jobs, which should implicitly fulfill 224 // the job belongsTo of each allocation pointed at each job. 225 226 visit('/jobs'); 227 228 // Finally, visit a node to assert that the job name and task group name are 229 // present. This will require reloading the job, since task groups aren't a 230 // part of the jobs list response. 231 232 visit(`/clients/${node.id}`); 233 234 andThen(() => { 235 const allocationRow = $(find('[data-test-allocation]')); 236 const allocation = server.db.allocations 237 .where({ nodeId: node.id }) 238 .sortBy('modifyIndex') 239 .reverse()[0]; 240 241 assert.ok( 242 allocationRow 243 .find('[data-test-job]') 244 .text() 245 .includes(server.db.jobs.find(allocation.jobId).name), 246 'Job name' 247 ); 248 assert.ok( 249 allocationRow 250 .find('[data-test-task-group]') 251 .text() 252 .includes(allocation.taskGroup), 253 'Task group name' 254 ); 255 }); 256 }); 257 258 test('each allocation should link to the allocation detail page', function(assert) { 259 const allocation = server.db.allocations 260 .where({ nodeId: node.id }) 261 .sortBy('modifyIndex') 262 .reverse()[0]; 263 264 visit(`/clients/${node.id}`); 265 266 andThen(() => { 267 click('[data-test-short-id] a'); 268 }); 269 270 andThen(() => { 271 assert.equal( 272 currentURL(), 273 `/allocations/${allocation.id}`, 274 'Allocation rows link to allocation detail pages' 275 ); 276 }); 277 }); 278 279 test('each allocation should link to the job the allocation belongs to', function(assert) { 280 visit(`/clients/${node.id}`); 281 282 const allocation = server.db.allocations.where({ nodeId: node.id })[0]; 283 const job = server.db.jobs.find(allocation.jobId); 284 285 andThen(() => { 286 click('[data-test-job]'); 287 }); 288 289 andThen(() => { 290 assert.equal( 291 currentURL(), 292 `/jobs/${job.id}`, 293 'Allocation rows link to the job detail page for the allocation' 294 ); 295 }); 296 }); 297 298 test('/clients/:id should list all attributes for the node', function(assert) { 299 visit(`/clients/${node.id}`); 300 301 andThen(() => { 302 assert.ok(find('[data-test-attributes]'), 'Attributes table is on the page'); 303 }); 304 }); 305 306 test('/clients/:id lists all meta attributes', function(assert) { 307 node = server.create('node', 'forceIPv4', 'withMeta'); 308 309 visit(`/clients/${node.id}`); 310 311 andThen(() => { 312 assert.ok(find('[data-test-meta]'), 'Meta attributes table is on the page'); 313 assert.notOk(find('[data-test-empty-meta-message]'), 'Meta attributes is not empty'); 314 315 const firstMetaKey = Object.keys(node.meta)[0]; 316 assert.equal( 317 find('[data-test-meta] [data-test-key]').textContent.trim(), 318 firstMetaKey, 319 'Meta attributes for the node are bound to the attributes table' 320 ); 321 assert.equal( 322 find('[data-test-meta] [data-test-value]').textContent.trim(), 323 node.meta[firstMetaKey], 324 'Meta attributes for the node are bound to the attributes table' 325 ); 326 }); 327 }); 328 329 test('/clients/:id shows an empty message when there is no meta data', function(assert) { 330 visit(`/clients/${node.id}`); 331 332 andThen(() => { 333 assert.notOk(find('[data-test-meta]'), 'Meta attributes table is not on the page'); 334 assert.ok(find('[data-test-empty-meta-message]'), 'Meta attributes is empty'); 335 }); 336 }); 337 338 test('when the node is not found, an error message is shown, but the URL persists', function(assert) { 339 visit('/clients/not-a-real-node'); 340 341 andThen(() => { 342 assert.equal( 343 server.pretender.handledRequests.findBy('status', 404).url, 344 '/v1/node/not-a-real-node', 345 'A request to the nonexistent node is made' 346 ); 347 assert.equal(currentURL(), '/clients/not-a-real-node', 'The URL persists'); 348 assert.ok(find('[data-test-error]'), 'Error message is shown'); 349 assert.equal( 350 find('[data-test-error-title]').textContent.trim(), 351 'Not Found', 352 'Error message is for 404' 353 ); 354 }); 355 }); 356 357 test('/clients/:id shows the recent events list', function(assert) { 358 visit(`/clients/${node.id}`); 359 360 andThen(() => { 361 assert.ok(find('[data-test-client-events]'), 'Client events section exists'); 362 }); 363 }); 364 365 test('each node event shows basic node event information', function(assert) { 366 const event = server.db.nodeEvents 367 .where({ nodeId: node.id }) 368 .sortBy('time') 369 .reverse()[0]; 370 371 visit(`/clients/${node.id}`); 372 373 andThen(() => { 374 const eventRow = $(find('[data-test-client-event]')); 375 assert.equal( 376 eventRow 377 .find('[data-test-client-event-time]') 378 .text() 379 .trim(), 380 moment(event.time).format('MM/DD/YY HH:mm:ss'), 381 'Event timestamp' 382 ); 383 assert.equal( 384 eventRow 385 .find('[data-test-client-event-subsystem]') 386 .text() 387 .trim(), 388 event.subsystem, 389 'Event subsystem' 390 ); 391 assert.equal( 392 eventRow 393 .find('[data-test-client-event-message]') 394 .text() 395 .trim(), 396 event.message, 397 'Event message' 398 ); 399 }); 400 }); 401 402 test('/clients/:id shows the driver status of every driver for the node', function(assert) { 403 // Set the drivers up so health and detection is well tested 404 const nodeDrivers = node.drivers; 405 const undetectedDriver = 'raw_exec'; 406 407 Object.values(nodeDrivers).forEach(driver => { 408 driver.Detected = true; 409 }); 410 411 nodeDrivers[undetectedDriver].Detected = false; 412 node.drivers = nodeDrivers; 413 414 const drivers = Object.keys(node.drivers) 415 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 416 .sortBy('Name'); 417 418 assert.ok(drivers.length > 0, 'Node has drivers'); 419 420 visit(`/clients/${node.id}`); 421 422 andThen(() => { 423 const driverRows = findAll('[data-test-driver-status] [data-test-accordion-head]'); 424 425 drivers.forEach((driver, index) => { 426 const driverRow = $(driverRows[index]); 427 428 assert.equal( 429 driverRow 430 .find('[data-test-name]') 431 .text() 432 .trim(), 433 driver.Name, 434 `${driver.Name}: Name is correct` 435 ); 436 assert.equal( 437 driverRow 438 .find('[data-test-detected]') 439 .text() 440 .trim(), 441 driver.Detected ? 'Yes' : 'No', 442 `${driver.Name}: Detection is correct` 443 ); 444 assert.equal( 445 driverRow 446 .find('[data-test-last-updated]') 447 .text() 448 .trim(), 449 moment(driver.UpdateTime).fromNow(), 450 `${driver.Name}: Last updated shows time since now` 451 ); 452 453 if (driver.Name === undetectedDriver) { 454 assert.notOk( 455 driverRow.find('[data-test-health]').length, 456 `${driver.Name}: No health for the undetected driver` 457 ); 458 } else { 459 assert.equal( 460 driverRow 461 .find('[data-test-health]') 462 .text() 463 .trim(), 464 driver.Healthy ? 'Healthy' : 'Unhealthy', 465 `${driver.Name}: Health is correct` 466 ); 467 assert.ok( 468 driverRow 469 .find('[data-test-health] .color-swatch') 470 .hasClass(driver.Healthy ? 'running' : 'failed'), 471 `${driver.Name}: Swatch with correct class is shown` 472 ); 473 } 474 }); 475 }); 476 }); 477 478 test('each driver can be opened to see a message and attributes', function(assert) { 479 // Only detected drivers can be expanded 480 const nodeDrivers = node.drivers; 481 Object.values(nodeDrivers).forEach(driver => { 482 driver.Detected = true; 483 }); 484 node.drivers = nodeDrivers; 485 486 const driver = Object.keys(node.drivers) 487 .map(driverName => assign({ Name: driverName }, node.drivers[driverName])) 488 .sortBy('Name')[0]; 489 490 visit(`/clients/${node.id}`); 491 492 andThen(() => { 493 const driverBody = $(find('[data-test-driver-status] [data-test-accordion-body]')); 494 assert.notOk( 495 driverBody.find('[data-test-health-description]').length, 496 'Driver health description is not shown' 497 ); 498 assert.notOk( 499 driverBody.find('[data-test-driver-attributes]').length, 500 'Driver attributes section is not shown' 501 ); 502 click('[data-test-driver-status] [data-test-accordion-toggle]'); 503 }); 504 505 andThen(() => { 506 const driverBody = $(find('[data-test-driver-status] [data-test-accordion-body]')); 507 assert.equal( 508 driverBody 509 .find('[data-test-health-description]') 510 .text() 511 .trim(), 512 driver.HealthDescription, 513 'Driver health description is now shown' 514 ); 515 assert.ok( 516 driverBody.find('[data-test-driver-attributes]').length, 517 'Driver attributes section is now shown' 518 ); 519 }); 520 }); 521 522 test('the status light indicates when the node is ineligible for scheduling', function(assert) { 523 node = server.create('node', { 524 schedulingEligibility: 'ineligible', 525 }); 526 527 visit(`/clients/${node.id}`); 528 529 andThen(() => { 530 assert.ok( 531 find('[data-test-node-status="ineligible"]'), 532 'Title status light is in the ineligible state' 533 ); 534 }); 535 }); 536 537 test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', function(assert) { 538 const deadline = 5400000000000; // 1.5 hours in nanoseconds 539 const forceDeadline = moment().add(1, 'd'); 540 541 node = server.create('node', { 542 drain: true, 543 schedulingEligibility: 'ineligible', 544 drainStrategy: { 545 Deadline: deadline, 546 ForceDeadline: forceDeadline.toISOString(), 547 IgnoreSystemJobs: false, 548 }, 549 }); 550 551 visit(`/clients/${node.id}`); 552 553 andThen(() => { 554 assert.ok( 555 find('[data-test-drain-deadline]') 556 .textContent.trim() 557 .includes(formatDuration(deadline)), 558 'Deadline is shown in a human formatted way' 559 ); 560 561 assert.ok( 562 find('[data-test-drain-forced-deadline]') 563 .textContent.trim() 564 .includes(forceDeadline.format('MM/DD/YY HH:mm:ss')), 565 'Force deadline is shown as an absolute date' 566 ); 567 568 assert.ok( 569 find('[data-test-drain-forced-deadline]') 570 .textContent.trim() 571 .includes(forceDeadline.fromNow()), 572 'Force deadline is shown as a relative date' 573 ); 574 575 assert.ok( 576 find('[data-test-drain-ignore-system-jobs]') 577 .textContent.trim() 578 .endsWith('No'), 579 'Ignore System Jobs state is shown' 580 ); 581 }); 582 }); 583 584 test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', function(assert) { 585 const deadline = 0; 586 587 node = server.create('node', { 588 drain: true, 589 schedulingEligibility: 'ineligible', 590 drainStrategy: { 591 Deadline: deadline, 592 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 593 IgnoreSystemJobs: true, 594 }, 595 }); 596 597 visit(`/clients/${node.id}`); 598 599 andThen(() => { 600 assert.ok( 601 find('[data-test-drain-deadline]') 602 .textContent.trim() 603 .includes('No deadline'), 604 'The value for Deadline is "no deadline"' 605 ); 606 607 assert.notOk( 608 find('[data-test-drain-forced-deadline]'), 609 'Forced deadline is not shown since there is no forced deadline' 610 ); 611 612 assert.ok( 613 find('[data-test-drain-ignore-system-jobs]') 614 .textContent.trim() 615 .endsWith('Yes'), 616 'Ignore System Jobs state is shown' 617 ); 618 }); 619 }); 620 621 test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', function(assert) { 622 const deadline = -1; 623 624 node = server.create('node', { 625 drain: true, 626 schedulingEligibility: 'ineligible', 627 drainStrategy: { 628 Deadline: deadline, 629 ForceDeadline: '0001-01-01T00:00:00Z', // null as a date 630 IgnoreSystemJobs: false, 631 }, 632 }); 633 634 visit(`/clients/${node.id}`); 635 636 andThen(() => { 637 assert.ok( 638 find('[data-test-drain-deadline] .badge.is-danger') 639 .textContent.trim() 640 .includes('Forced Drain'), 641 'Forced Drain is shown in a red badge' 642 ); 643 644 assert.notOk( 645 find('[data-test-drain-forced-deadline]'), 646 'Forced deadline is not shown since there is no forced deadline' 647 ); 648 649 assert.ok( 650 find('[data-test-drain-ignore-system-jobs]') 651 .textContent.trim() 652 .endsWith('No'), 653 'Ignore System Jobs state is shown' 654 ); 655 }); 656 });