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