github.com/blixtra/nomad@v0.7.2-0.20171221000451-da9a1d7bb050/ui/tests/acceptance/job-detail-test.js (about) 1 import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers'; 2 import Ember from 'ember'; 3 import moment from 'moment'; 4 import { test } from 'qunit'; 5 import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; 6 7 const { get, $ } = Ember; 8 const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); 9 10 let job; 11 12 moduleForAcceptance('Acceptance | job detail', { 13 beforeEach() { 14 server.create('node'); 15 job = server.create('job', { type: 'service' }); 16 visit(`/jobs/${job.id}`); 17 }, 18 }); 19 20 test('visiting /jobs/:job_id', function(assert) { 21 assert.equal(currentURL(), `/jobs/${job.id}`); 22 }); 23 24 test('breadcrumbs includes job name and link back to the jobs list', function(assert) { 25 assert.equal(findAll('.breadcrumb')[0].textContent, 'Jobs', 'First breadcrumb says jobs'); 26 assert.equal( 27 findAll('.breadcrumb')[1].textContent, 28 job.name, 29 'Second breadcrumb says the job name' 30 ); 31 32 click(findAll('.breadcrumb')[0]); 33 andThen(() => { 34 assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); 35 }); 36 }); 37 38 test('the subnav includes links to definition, versions, and deployments when type = service', function( 39 assert 40 ) { 41 const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent); 42 assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); 43 assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); 44 assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); 45 }); 46 47 test('the subnav includes links to definition and versions when type != service', function(assert) { 48 job = server.create('job', { type: 'batch' }); 49 visit(`/jobs/${job.id}`); 50 51 andThen(() => { 52 const subnavLabels = findAll('.tabs.is-subnav a').map(anchor => anchor.textContent); 53 assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); 54 assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); 55 assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); 56 }); 57 }); 58 59 test('the job detail page should contain basic information about the job', function(assert) { 60 assert.ok(findAll('.title .tag')[0].textContent.includes(job.status), 'Status'); 61 assert.ok(findAll('.job-stats span')[0].textContent.includes(job.type), 'Type'); 62 assert.ok(findAll('.job-stats span')[1].textContent.includes(job.priority), 'Priority'); 63 assert.notOk(findAll('.job-stats span')[2], 'Namespace is not included'); 64 }); 65 66 test('the job detail page should list all task groups', function(assert) { 67 assert.equal( 68 findAll('.task-group-row').length, 69 server.db.taskGroups.where({ jobId: job.id }).length 70 ); 71 }); 72 73 test('each row in the task group table should show basic information about the task group', function( 74 assert 75 ) { 76 const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0]; 77 const taskGroupRow = $(findAll('.task-group-row')[0]); 78 const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id }); 79 const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); 80 81 assert.equal( 82 taskGroupRow 83 .find('td:eq(0)') 84 .text() 85 .trim(), 86 taskGroup.name, 87 'Name' 88 ); 89 assert.equal( 90 taskGroupRow 91 .find('td:eq(1)') 92 .text() 93 .trim(), 94 taskGroup.count, 95 'Count' 96 ); 97 assert.equal( 98 taskGroupRow.find('td:eq(3)').text(), 99 `${sum(tasks, 'Resources.CPU')} MHz`, 100 'Reserved CPU' 101 ); 102 assert.equal( 103 taskGroupRow.find('td:eq(4)').text(), 104 `${sum(tasks, 'Resources.MemoryMB')} MiB`, 105 'Reserved Memory' 106 ); 107 assert.equal( 108 taskGroupRow.find('td:eq(5)').text(), 109 `${taskGroup.ephemeralDisk.SizeMB} MiB`, 110 'Reserved Disk' 111 ); 112 }); 113 114 test('the allocations diagram lists all allocation status figures', function(assert) { 115 const legend = find('.distribution-bar .legend'); 116 const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id }); 117 const statusCounts = Object.keys(jobSummary.Summary).reduce( 118 (counts, key) => { 119 const group = jobSummary.Summary[key]; 120 counts.queued += group.Queued; 121 counts.starting += group.Starting; 122 counts.running += group.Running; 123 counts.complete += group.Complete; 124 counts.failed += group.Failed; 125 counts.lost += group.Lost; 126 return counts; 127 }, 128 { queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 } 129 ); 130 131 assert.equal( 132 legend.querySelector('li.queued .value').textContent, 133 statusCounts.queued, 134 `${statusCounts.queued} are queued` 135 ); 136 137 assert.equal( 138 legend.querySelector('li.starting .value').textContent, 139 statusCounts.starting, 140 `${statusCounts.starting} are starting` 141 ); 142 143 assert.equal( 144 legend.querySelector('li.running .value').textContent, 145 statusCounts.running, 146 `${statusCounts.running} are running` 147 ); 148 149 assert.equal( 150 legend.querySelector('li.complete .value').textContent, 151 statusCounts.complete, 152 `${statusCounts.complete} are complete` 153 ); 154 155 assert.equal( 156 legend.querySelector('li.failed .value').textContent, 157 statusCounts.failed, 158 `${statusCounts.failed} are failed` 159 ); 160 161 assert.equal( 162 legend.querySelector('li.lost .value').textContent, 163 statusCounts.lost, 164 `${statusCounts.lost} are lost` 165 ); 166 }); 167 168 test('there is no active deployment section when the job has no active deployment', function( 169 assert 170 ) { 171 // TODO: it would be better to not visit two different job pages in one test, but this 172 // way is much more convenient. 173 job = server.create('job', { noActiveDeployment: true, type: 'service' }); 174 visit(`/jobs/${job.id}`); 175 176 andThen(() => { 177 assert.ok(findAll('.active-deployment').length === 0, 'No active deployment'); 178 }); 179 }); 180 181 test('the active deployment section shows up for the currently running deployment', function( 182 assert 183 ) { 184 job = server.create('job', { activeDeployment: true, type: 'service' }); 185 const deployment = server.db.deployments.where({ jobId: job.id })[0]; 186 const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({ 187 deploymentId: deployment.id, 188 }); 189 const version = server.db.jobVersions.findBy({ 190 jobId: job.id, 191 version: deployment.versionNumber, 192 }); 193 visit(`/jobs/${job.id}`); 194 195 andThen(() => { 196 assert.ok(findAll('.active-deployment').length === 1, 'Active deployment'); 197 assert.equal( 198 $('.active-deployment > .boxed-section-head .badge') 199 .get(0) 200 .textContent.trim(), 201 deployment.id.split('-')[0], 202 'The active deployment is the most recent running deployment' 203 ); 204 205 assert.equal( 206 $('.active-deployment > .boxed-section-head .submit-time') 207 .get(0) 208 .textContent.trim(), 209 moment(version.submitTime / 1000000).fromNow(), 210 'Time since the job was submitted is in the active deployment header' 211 ); 212 213 assert.equal( 214 $('.deployment-metrics .label:contains("Canaries") + .value') 215 .get(0) 216 .textContent.trim(), 217 `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum( 218 taskGroupSummaries, 219 'desiredCanaries' 220 )}`, 221 'Canaries, both places and desired, are in the metrics' 222 ); 223 224 assert.equal( 225 $('.deployment-metrics .label:contains("Placed") + .value') 226 .get(0) 227 .textContent.trim(), 228 sum(taskGroupSummaries, 'placedAllocs'), 229 'Placed allocs aggregates across task groups' 230 ); 231 232 assert.equal( 233 $('.deployment-metrics .label:contains("Desired") + .value') 234 .get(0) 235 .textContent.trim(), 236 sum(taskGroupSummaries, 'desiredTotal'), 237 'Desired allocs aggregates across task groups' 238 ); 239 240 assert.equal( 241 $('.deployment-metrics .label:contains("Healthy") + .value') 242 .get(0) 243 .textContent.trim(), 244 sum(taskGroupSummaries, 'healthyAllocs'), 245 'Healthy allocs aggregates across task groups' 246 ); 247 248 assert.equal( 249 $('.deployment-metrics .label:contains("Unhealthy") + .value') 250 .get(0) 251 .textContent.trim(), 252 sum(taskGroupSummaries, 'unhealthyAllocs'), 253 'Unhealthy allocs aggregates across task groups' 254 ); 255 256 assert.equal( 257 $('.deployment-metrics .notification') 258 .get(0) 259 .textContent.trim(), 260 deployment.statusDescription, 261 'Status description is in the metrics block' 262 ); 263 }); 264 }); 265 266 test('the active deployment section can be expanded to show task groups and allocations', function( 267 assert 268 ) { 269 job = server.create('job', { activeDeployment: true, type: 'service' }); 270 visit(`/jobs/${job.id}`); 271 272 andThen(() => { 273 assert.ok( 274 $('.active-deployment .boxed-section-head:contains("Task Groups")').length === 0, 275 'Task groups not found' 276 ); 277 assert.ok( 278 $('.active-deployment .boxed-section-head:contains("Allocations")').length === 0, 279 'Allocations not found' 280 ); 281 }); 282 283 andThen(() => { 284 click('.active-deployment-details-toggle'); 285 }); 286 287 andThen(() => { 288 assert.ok( 289 $('.active-deployment .boxed-section-head:contains("Task Groups")').length === 1, 290 'Task groups found' 291 ); 292 assert.ok( 293 $('.active-deployment .boxed-section-head:contains("Allocations")').length === 1, 294 'Allocations found' 295 ); 296 }); 297 }); 298 299 test('the evaluations table lists evaluations sorted by modify index', function(assert) { 300 job = server.create('job'); 301 const evaluations = server.db.evaluations 302 .where({ jobId: job.id }) 303 .sortBy('modifyIndex') 304 .reverse(); 305 306 visit(`/jobs/${job.id}`); 307 308 andThen(() => { 309 assert.equal( 310 findAll('.evaluations tbody tr').length, 311 evaluations.length, 312 'A row for each evaluation' 313 ); 314 315 evaluations.forEach((evaluation, index) => { 316 const row = $(findAll('.evaluations tbody tr')[index]); 317 assert.equal( 318 row.find('td:eq(0)').text(), 319 evaluation.id.split('-')[0], 320 `Short ID, row ${index}` 321 ); 322 }); 323 324 const firstEvaluation = evaluations[0]; 325 const row = $(findAll('.evaluations tbody tr')[0]); 326 assert.equal(row.find('td:eq(1)').text(), '' + firstEvaluation.priority, 'Priority'); 327 assert.equal(row.find('td:eq(2)').text(), firstEvaluation.triggeredBy, 'Triggered By'); 328 assert.equal(row.find('td:eq(3)').text(), firstEvaluation.status, 'Status'); 329 }); 330 }); 331 332 test('when the job has placement failures, they are called out', function(assert) { 333 job = server.create('job', { failedPlacements: true }); 334 const failedEvaluation = server.db.evaluations 335 .where({ jobId: job.id }) 336 .filter(evaluation => evaluation.failedTGAllocs) 337 .sortBy('modifyIndex') 338 .reverse()[0]; 339 340 const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs); 341 342 visit(`/jobs/${job.id}`); 343 344 andThen(() => { 345 assert.ok(find('.placement-failures'), 'Placement failures section found'); 346 347 const taskGroupLabels = findAll('.placement-failures h3.title').map(title => 348 title.textContent.trim() 349 ); 350 failedTaskGroupNames.forEach(name => { 351 assert.ok( 352 taskGroupLabels.find(label => label.includes(name)), 353 `${name} included in placement failures list` 354 ); 355 assert.ok( 356 taskGroupLabels.find(label => 357 label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1) 358 ), 359 'The number of unplaced allocs = CoalescedFailures + 1' 360 ); 361 }); 362 }); 363 }); 364 365 test('when the job has no placement failures, the placement failures section is gone', function( 366 assert 367 ) { 368 job = server.create('job', { noFailedPlacements: true }); 369 visit(`/jobs/${job.id}`); 370 371 andThen(() => { 372 assert.notOk(find('.placement-failures'), 'Placement failures section not found'); 373 }); 374 }); 375 376 test('when the job is not found, an error message is shown, but the URL persists', function( 377 assert 378 ) { 379 visit('/jobs/not-a-real-job'); 380 381 andThen(() => { 382 assert.equal( 383 server.pretender.handledRequests.findBy('status', 404).url, 384 '/v1/job/not-a-real-job', 385 'A request to the non-existent job is made' 386 ); 387 assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists'); 388 assert.ok(find('.error-message'), 'Error message is shown'); 389 assert.equal( 390 find('.error-message .title').textContent, 391 'Not Found', 392 'Error message is for 404' 393 ); 394 }); 395 }); 396 397 moduleForAcceptance('Acceptance | job detail (with namespaces)', { 398 beforeEach() { 399 server.createList('namespace', 2); 400 server.create('node'); 401 job = server.create('job', { namespaceId: server.db.namespaces[1].name }); 402 server.createList('job', 3, { namespaceId: server.db.namespaces[0].name }); 403 }, 404 }); 405 406 test('when there are namespaces, the job detail page states the namespace for the job', function( 407 assert 408 ) { 409 const namespace = server.db.namespaces.find(job.namespaceId); 410 visit(`/jobs/${job.id}?namespace=${namespace.name}`); 411 412 andThen(() => { 413 assert.ok( 414 findAll('.job-stats span')[2].textContent.includes(namespace.name), 415 'Namespace included in stats' 416 ); 417 }); 418 }); 419 420 test('when switching namespaces, the app redirects to /jobs with the new namespace', function( 421 assert 422 ) { 423 const namespace = server.db.namespaces.find(job.namespaceId); 424 const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name; 425 const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace; 426 427 visit(`/jobs/${job.id}?namespace=${namespace.name}`); 428 429 andThen(() => { 430 selectChoose('.namespace-switcher', label); 431 }); 432 433 andThen(() => { 434 assert.equal(currentURL().split('?')[0], '/jobs', 'Navigated to /jobs'); 435 const jobs = server.db.jobs 436 .where({ namespace: otherNamespace }) 437 .sortBy('modifyIndex') 438 .reverse(); 439 assert.equal(findAll('.job-row').length, jobs.length, 'Shows the right number of jobs'); 440 jobs.forEach((job, index) => { 441 assert.equal( 442 $(findAll('.job-row')[index]) 443 .find('td:eq(0)') 444 .text() 445 .trim(), 446 job.name, 447 `Job ${index} is right` 448 ); 449 }); 450 }); 451 });