github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/jobs-list-test.js (about) 1 import { currentURL } from '@ember/test-helpers'; 2 import { module, test } from 'qunit'; 3 import { setupApplicationTest } from 'ember-qunit'; 4 import { setupMirage } from 'ember-cli-mirage/test-support'; 5 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 6 import pageSizeSelect from './behaviors/page-size-select'; 7 import JobsList from 'nomad-ui/tests/pages/jobs/list'; 8 import Layout from 'nomad-ui/tests/pages/layout'; 9 10 let managementToken, clientToken; 11 12 module('Acceptance | jobs list', function(hooks) { 13 setupApplicationTest(hooks); 14 setupMirage(hooks); 15 16 hooks.beforeEach(function() { 17 // Required for placing allocations (a result of creating jobs) 18 server.create('node'); 19 20 managementToken = server.create('token'); 21 clientToken = server.create('token'); 22 23 window.localStorage.clear(); 24 window.localStorage.nomadTokenSecret = managementToken.secretId; 25 }); 26 27 test('it passes an accessibility audit', async function(assert) { 28 await JobsList.visit(); 29 await a11yAudit(assert); 30 }); 31 32 test('visiting /jobs', async function(assert) { 33 await JobsList.visit(); 34 35 assert.equal(currentURL(), '/jobs'); 36 assert.equal(document.title, 'Jobs - Nomad'); 37 }); 38 39 test('/jobs should list the first page of jobs sorted by modify index', async function(assert) { 40 const jobsCount = JobsList.pageSize + 1; 41 server.createList('job', jobsCount, { createAllocations: false }); 42 43 await JobsList.visit(); 44 45 const sortedJobs = server.db.jobs.sortBy('modifyIndex').reverse(); 46 assert.equal(JobsList.jobs.length, JobsList.pageSize); 47 JobsList.jobs.forEach((job, index) => { 48 assert.equal(job.name, sortedJobs[index].name, 'Jobs are ordered'); 49 }); 50 }); 51 52 test('each job row should contain information about the job', async function(assert) { 53 server.createList('job', 2); 54 const job = server.db.jobs.sortBy('modifyIndex').reverse()[0]; 55 const taskGroups = server.db.taskGroups.where({ jobId: job.id }); 56 57 await JobsList.visit(); 58 59 const jobRow = JobsList.jobs.objectAt(0); 60 61 assert.equal(jobRow.name, job.name, 'Name'); 62 assert.equal(jobRow.link, `/ui/jobs/${job.id}`, 'Detail Link'); 63 assert.equal(jobRow.status, job.status, 'Status'); 64 assert.equal(jobRow.type, typeForJob(job), 'Type'); 65 assert.equal(jobRow.priority, job.priority, 'Priority'); 66 assert.equal(jobRow.taskGroups, taskGroups.length, '# Groups'); 67 }); 68 69 test('each job row should link to the corresponding job', async function(assert) { 70 server.create('job'); 71 const job = server.db.jobs[0]; 72 73 await JobsList.visit(); 74 await JobsList.jobs.objectAt(0).clickName(); 75 76 assert.equal(currentURL(), `/jobs/${job.id}`); 77 }); 78 79 test('the new job button transitions to the new job page', async function(assert) { 80 await JobsList.visit(); 81 await JobsList.runJobButton.click(); 82 83 assert.equal(currentURL(), '/jobs/run'); 84 }); 85 86 test('the job run button is disabled when the token lacks permission', async function(assert) { 87 window.localStorage.nomadTokenSecret = clientToken.secretId; 88 await JobsList.visit(); 89 90 assert.ok(JobsList.runJobButton.isDisabled); 91 92 await JobsList.runJobButton.click(); 93 assert.equal(currentURL(), '/jobs'); 94 }); 95 96 test('the job run button state can change between namespaces', async function(assert) { 97 server.createList('namespace', 2); 98 const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id }); 99 const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id }); 100 101 window.localStorage.nomadTokenSecret = clientToken.secretId; 102 103 const policy = server.create('policy', { 104 id: 'something', 105 name: 'something', 106 rulesJSON: { 107 Namespaces: [ 108 { 109 Name: job1.namespaceId, 110 Capabilities: ['list-jobs', 'submit-job'], 111 }, 112 { 113 Name: job2.namespaceId, 114 Capabilities: ['list-jobs'], 115 }, 116 ], 117 }, 118 }); 119 120 clientToken.policyIds = [policy.id]; 121 clientToken.save(); 122 123 await JobsList.visit(); 124 assert.notOk(JobsList.runJobButton.isDisabled); 125 126 const secondNamespace = server.db.namespaces[1]; 127 await JobsList.visit({ namespace: secondNamespace.id }); 128 assert.ok(JobsList.runJobButton.isDisabled); 129 }); 130 131 test('the anonymous policy is fetched to check whether to show the job run button', async function(assert) { 132 window.localStorage.removeItem('nomadTokenSecret'); 133 134 server.create('policy', { 135 id: 'anonymous', 136 name: 'anonymous', 137 rulesJSON: { 138 Namespaces: [ 139 { 140 Name: 'default', 141 Capabilities: ['list-jobs', 'submit-job'], 142 }, 143 ], 144 }, 145 }); 146 147 await JobsList.visit(); 148 assert.notOk(JobsList.runJobButton.isDisabled); 149 }); 150 151 test('when there are no jobs, there is an empty message', async function(assert) { 152 await JobsList.visit(); 153 154 assert.ok(JobsList.isEmpty, 'There is an empty message'); 155 assert.equal(JobsList.emptyState.headline, 'No Jobs', 'The message is appropriate'); 156 }); 157 158 test('when there are jobs, but no matches for a search result, there is an empty message', async function(assert) { 159 server.create('job', { name: 'cat 1' }); 160 server.create('job', { name: 'cat 2' }); 161 162 await JobsList.visit(); 163 164 await JobsList.search.fillIn('dog'); 165 assert.ok(JobsList.isEmpty, 'The empty message is shown'); 166 assert.equal(JobsList.emptyState.headline, 'No Matches', 'The message is appropriate'); 167 }); 168 169 test('searching resets the current page', async function(assert) { 170 server.createList('job', JobsList.pageSize + 1, { createAllocations: false }); 171 172 await JobsList.visit(); 173 await JobsList.nextPage(); 174 175 assert.equal(currentURL(), '/jobs?page=2', 'Page query param captures page=2'); 176 177 await JobsList.search.fillIn('foobar'); 178 179 assert.equal(currentURL(), '/jobs?search=foobar', 'No page query param'); 180 }); 181 182 test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', async function(assert) { 183 server.createList('namespace', 2); 184 const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id }); 185 const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id }); 186 187 await JobsList.visit(); 188 189 assert.equal(JobsList.jobs.length, 1, 'One job in the default namespace'); 190 assert.equal(JobsList.jobs.objectAt(0).name, job1.name, 'The correct job is shown'); 191 192 const secondNamespace = server.db.namespaces[1]; 193 await JobsList.visit({ namespace: secondNamespace.id }); 194 195 assert.equal(JobsList.jobs.length, 1, `One job in the ${secondNamespace.name} namespace`); 196 assert.equal(JobsList.jobs.objectAt(0).name, job2.name, 'The correct job is shown'); 197 }); 198 199 test('when accessing jobs is forbidden, show a message with a link to the tokens page', async function(assert) { 200 server.pretender.get('/v1/jobs', () => [403, {}, null]); 201 202 await JobsList.visit(); 203 assert.equal(JobsList.error.title, 'Not Authorized'); 204 205 await JobsList.error.seekHelp(); 206 assert.equal(currentURL(), '/settings/tokens'); 207 }); 208 209 function typeForJob(job) { 210 return job.periodic ? 'periodic' : job.parameterized ? 'parameterized' : job.type; 211 } 212 213 test('the jobs list page has appropriate faceted search options', async function(assert) { 214 await JobsList.visit(); 215 216 assert.ok(JobsList.facets.type.isPresent, 'Type facet found'); 217 assert.ok(JobsList.facets.status.isPresent, 'Status facet found'); 218 assert.ok(JobsList.facets.datacenter.isPresent, 'Datacenter facet found'); 219 assert.ok(JobsList.facets.prefix.isPresent, 'Prefix facet found'); 220 }); 221 222 testFacet('Type', { 223 facet: JobsList.facets.type, 224 paramName: 'type', 225 expectedOptions: ['Batch', 'Parameterized', 'Periodic', 'Service', 'System'], 226 async beforeEach() { 227 server.createList('job', 2, { createAllocations: false, type: 'batch' }); 228 server.createList('job', 2, { 229 createAllocations: false, 230 type: 'batch', 231 periodic: true, 232 childrenCount: 0, 233 }); 234 server.createList('job', 2, { 235 createAllocations: false, 236 type: 'batch', 237 parameterized: true, 238 childrenCount: 0, 239 }); 240 server.createList('job', 2, { createAllocations: false, type: 'service' }); 241 await JobsList.visit(); 242 }, 243 filter(job, selection) { 244 let displayType = job.type; 245 if (job.parameterized) displayType = 'parameterized'; 246 if (job.periodic) displayType = 'periodic'; 247 return selection.includes(displayType); 248 }, 249 }); 250 251 testFacet('Status', { 252 facet: JobsList.facets.status, 253 paramName: 'status', 254 expectedOptions: ['Pending', 'Running', 'Dead'], 255 async beforeEach() { 256 server.createList('job', 2, { 257 status: 'pending', 258 createAllocations: false, 259 childrenCount: 0, 260 }); 261 server.createList('job', 2, { 262 status: 'running', 263 createAllocations: false, 264 childrenCount: 0, 265 }); 266 server.createList('job', 2, { status: 'dead', createAllocations: false, childrenCount: 0 }); 267 await JobsList.visit(); 268 }, 269 filter: (job, selection) => selection.includes(job.status), 270 }); 271 272 testFacet('Datacenter', { 273 facet: JobsList.facets.datacenter, 274 paramName: 'dc', 275 expectedOptions(jobs) { 276 const allDatacenters = new Set( 277 jobs.mapBy('datacenters').reduce((acc, val) => acc.concat(val), []) 278 ); 279 return Array.from(allDatacenters).sort(); 280 }, 281 async beforeEach() { 282 server.create('job', { 283 datacenters: ['pdx', 'lax'], 284 createAllocations: false, 285 childrenCount: 0, 286 }); 287 server.create('job', { 288 datacenters: ['pdx', 'ord'], 289 createAllocations: false, 290 childrenCount: 0, 291 }); 292 server.create('job', { 293 datacenters: ['lax', 'jfk'], 294 createAllocations: false, 295 childrenCount: 0, 296 }); 297 server.create('job', { 298 datacenters: ['jfk', 'dfw'], 299 createAllocations: false, 300 childrenCount: 0, 301 }); 302 server.create('job', { datacenters: ['pdx'], createAllocations: false, childrenCount: 0 }); 303 await JobsList.visit(); 304 }, 305 filter: (job, selection) => job.datacenters.find(dc => selection.includes(dc)), 306 }); 307 308 testFacet('Prefix', { 309 facet: JobsList.facets.prefix, 310 paramName: 'prefix', 311 expectedOptions: ['hashi (3)', 'nmd (2)', 'pre (2)'], 312 async beforeEach() { 313 [ 314 'pre-one', 315 'hashi_one', 316 'nmd.one', 317 'one-alone', 318 'pre_two', 319 'hashi.two', 320 'hashi-three', 321 'nmd_two', 322 'noprefix', 323 ].forEach(name => { 324 server.create('job', { name, createAllocations: false, childrenCount: 0 }); 325 }); 326 await JobsList.visit(); 327 }, 328 filter: (job, selection) => selection.find(prefix => job.name.startsWith(prefix)), 329 }); 330 331 test('when the facet selections result in no matches, the empty state states why', async function(assert) { 332 server.createList('job', 2, { status: 'pending', createAllocations: false, childrenCount: 0 }); 333 334 await JobsList.visit(); 335 336 await JobsList.facets.status.toggle(); 337 await JobsList.facets.status.options.objectAt(1).toggle(); 338 assert.ok(JobsList.isEmpty, 'There is an empty message'); 339 assert.equal(JobsList.emptyState.headline, 'No Matches', 'The message is appropriate'); 340 }); 341 342 test('the jobs list is immediately filtered based on query params', async function(assert) { 343 server.create('job', { type: 'batch', createAllocations: false }); 344 server.create('job', { type: 'service', createAllocations: false }); 345 346 await JobsList.visit({ type: JSON.stringify(['batch']) }); 347 348 assert.equal(JobsList.jobs.length, 1, 'Only one job shown due to query param'); 349 }); 350 351 test('the active namespace is carried over to the storage pages', async function(assert) { 352 server.createList('namespace', 2); 353 354 const namespace = server.db.namespaces[1]; 355 await JobsList.visit({ namespace: namespace.id }); 356 357 await Layout.gutter.visitStorage(); 358 359 assert.equal(currentURL(), `/csi/volumes?namespace=${namespace.id}`); 360 }); 361 362 pageSizeSelect({ 363 resourceName: 'job', 364 pageObject: JobsList, 365 pageObjectList: JobsList.jobs, 366 async setup() { 367 server.createList('job', JobsList.pageSize, { shallow: true, createAllocations: false }); 368 await JobsList.visit(); 369 }, 370 }); 371 372 function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) { 373 test(`the ${label} facet has the correct options`, async function(assert) { 374 await beforeEach(); 375 await facet.toggle(); 376 377 let expectation; 378 if (typeof expectedOptions === 'function') { 379 expectation = expectedOptions(server.db.jobs); 380 } else { 381 expectation = expectedOptions; 382 } 383 384 assert.deepEqual( 385 facet.options.map(option => option.label.trim()), 386 expectation, 387 'Options for facet are as expected' 388 ); 389 }); 390 391 test(`the ${label} facet filters the jobs list by ${label}`, async function(assert) { 392 let option; 393 394 await beforeEach(); 395 await facet.toggle(); 396 397 option = facet.options.objectAt(0); 398 await option.toggle(); 399 400 const selection = [option.key]; 401 const expectedJobs = server.db.jobs 402 .filter(job => filter(job, selection)) 403 .sortBy('modifyIndex') 404 .reverse(); 405 406 JobsList.jobs.forEach((job, index) => { 407 assert.equal( 408 job.id, 409 expectedJobs[index].id, 410 `Job at ${index} is ${expectedJobs[index].id}` 411 ); 412 }); 413 }); 414 415 test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) { 416 const selection = []; 417 418 await beforeEach(); 419 await facet.toggle(); 420 421 const option1 = facet.options.objectAt(0); 422 const option2 = facet.options.objectAt(1); 423 await option1.toggle(); 424 selection.push(option1.key); 425 await option2.toggle(); 426 selection.push(option2.key); 427 428 const expectedJobs = server.db.jobs 429 .filter(job => filter(job, selection)) 430 .sortBy('modifyIndex') 431 .reverse(); 432 433 JobsList.jobs.forEach((job, index) => { 434 assert.equal( 435 job.id, 436 expectedJobs[index].id, 437 `Job at ${index} is ${expectedJobs[index].id}` 438 ); 439 }); 440 }); 441 442 test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) { 443 const selection = []; 444 445 await beforeEach(); 446 await facet.toggle(); 447 448 const option1 = facet.options.objectAt(0); 449 const option2 = facet.options.objectAt(1); 450 await option1.toggle(); 451 selection.push(option1.key); 452 await option2.toggle(); 453 selection.push(option2.key); 454 455 assert.equal( 456 currentURL(), 457 `/jobs?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`, 458 'URL has the correct query param key and value' 459 ); 460 }); 461 462 test('the run job button works when filters are set', async function(assert) { 463 ['pre-one', 'pre-two', 'pre-three'].forEach(name => { 464 server.create('job', { name, createAllocations: false, childrenCount: 0 }); 465 }); 466 467 await JobsList.visit(); 468 469 await JobsList.facets.prefix.toggle(); 470 await JobsList.facets.prefix.options[0].toggle(); 471 472 await JobsList.runJobButton.click(); 473 assert.equal(currentURL(), '/jobs/run'); 474 }); 475 } 476 });