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