github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/job-allocations-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 /* eslint-disable qunit/require-expect */ 7 import { currentURL, click, find } from '@ember/test-helpers'; 8 import { module, test } from 'qunit'; 9 import { setupApplicationTest } from 'ember-qunit'; 10 import { setupMirage } from 'ember-cli-mirage/test-support'; 11 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 12 import Allocations from 'nomad-ui/tests/pages/jobs/job/allocations'; 13 14 let job; 15 let allocations; 16 17 const makeSearchAllocations = (server) => { 18 Array(10) 19 .fill(null) 20 .map((_, index) => { 21 server.create('allocation', { 22 id: index < 5 ? `ffffff-dddddd-${index}` : `111111-222222-${index}`, 23 shallow: true, 24 }); 25 }); 26 }; 27 28 module('Acceptance | job allocations', function (hooks) { 29 setupApplicationTest(hooks); 30 setupMirage(hooks); 31 32 hooks.beforeEach(function () { 33 server.create('node-pool'); 34 server.create('node'); 35 36 job = server.create('job', { 37 noFailedPlacements: true, 38 createAllocations: false, 39 }); 40 }); 41 42 test('it passes an accessibility audit', async function (assert) { 43 server.createList('allocation', Allocations.pageSize - 1, { 44 shallow: true, 45 }); 46 allocations = server.schema.allocations.where({ jobId: job.id }).models; 47 48 await Allocations.visit({ id: job.id }); 49 await a11yAudit(assert); 50 }); 51 52 test('lists all allocations for the job', async function (assert) { 53 server.createList('allocation', Allocations.pageSize - 1, { 54 shallow: true, 55 }); 56 allocations = server.schema.allocations.where({ jobId: job.id }).models; 57 58 await Allocations.visit({ id: job.id }); 59 60 assert.equal( 61 Allocations.allocations.length, 62 Allocations.pageSize - 1, 63 'Allocations are shown in a table' 64 ); 65 66 const sortedAllocations = allocations.sortBy('modifyIndex').reverse(); 67 68 Allocations.allocations.forEach((allocation, index) => { 69 const shortId = sortedAllocations[index].id.split('-')[0]; 70 assert.equal( 71 allocation.shortId, 72 shortId, 73 `Allocation ${index} is ${shortId}` 74 ); 75 }); 76 77 assert.equal(document.title, `Job ${job.name} allocations - Nomad`); 78 }); 79 80 test('clicking an allocation results in the correct endpoint being hit', async function (assert) { 81 server.createList('allocation', Allocations.pageSize - 1, { 82 shallow: true, 83 }); 84 allocations = server.schema.allocations.where({ jobId: job.id }).models; 85 86 await Allocations.visit({ id: job.id }); 87 88 const firstAllocation = find('[data-test-allocation]'); 89 await click(firstAllocation); 90 91 const requestToAllocationEndpoint = server.pretender.handledRequests.find( 92 (request) => 93 request.url.includes( 94 `/v1/allocation/${firstAllocation.dataset.testAllocation}` 95 ) 96 ); 97 98 assert.ok(requestToAllocationEndpoint, 'the correct endpoint is hit'); 99 100 assert.equal( 101 currentURL(), 102 `/allocations/${firstAllocation.dataset.testAllocation}`, 103 'the URL is correct' 104 ); 105 }); 106 107 test('allocations table is sortable', async function (assert) { 108 server.createList('allocation', Allocations.pageSize - 1); 109 allocations = server.schema.allocations.where({ jobId: job.id }).models; 110 111 await Allocations.visit({ id: job.id }); 112 await Allocations.sortBy('taskGroupName'); 113 114 assert.equal( 115 currentURL(), 116 `/jobs/${job.id}/allocations?sort=taskGroupName`, 117 'the URL persists the sort parameter' 118 ); 119 const sortedAllocations = allocations.sortBy('taskGroup').reverse(); 120 Allocations.allocations.forEach((allocation, index) => { 121 const shortId = sortedAllocations[index].id.split('-')[0]; 122 assert.equal( 123 allocation.shortId, 124 shortId, 125 `Allocation ${index} is ${shortId} with task group ${sortedAllocations[index].taskGroup}` 126 ); 127 }); 128 }); 129 130 test('allocations table is searchable', async function (assert) { 131 makeSearchAllocations(server); 132 133 allocations = server.schema.allocations.where({ jobId: job.id }).models; 134 135 await Allocations.visit({ id: job.id }); 136 await Allocations.search('ffffff'); 137 138 assert.equal( 139 Allocations.allocations.length, 140 5, 141 'List is filtered by search term' 142 ); 143 }); 144 145 test('when a search yields no results, the search box remains', async function (assert) { 146 makeSearchAllocations(server); 147 148 allocations = server.schema.allocations.where({ jobId: job.id }).models; 149 150 await Allocations.visit({ id: job.id }); 151 await Allocations.search('^nothing will ever match this long regex$'); 152 153 assert.equal( 154 Allocations.emptyState.headline, 155 'No Matches', 156 'List is empty and the empty state is about search' 157 ); 158 159 assert.ok(Allocations.hasSearchBox, 'Search box is still shown'); 160 }); 161 162 test('when the job for the allocations is not found, an error message is shown, but the URL persists', async function (assert) { 163 await Allocations.visit({ id: 'not-a-real-job' }); 164 165 assert.equal( 166 server.pretender.handledRequests 167 .filter((request) => !request.url.includes('policy')) 168 .findBy('status', 404).url, 169 '/v1/job/not-a-real-job', 170 'A request to the nonexistent job is made' 171 ); 172 assert.equal( 173 currentURL(), 174 '/jobs/not-a-real-job/allocations', 175 'The URL persists' 176 ); 177 assert.ok(Allocations.error.isPresent, 'Error message is shown'); 178 assert.equal( 179 Allocations.error.title, 180 'Not Found', 181 'Error message is for 404' 182 ); 183 }); 184 185 testFacet('Status', { 186 facet: Allocations.facets.status, 187 paramName: 'status', 188 expectedOptions: [ 189 'Pending', 190 'Running', 191 'Complete', 192 'Failed', 193 'Lost', 194 'Unknown', 195 ], 196 async beforeEach() { 197 ['pending', 'running', 'complete', 'failed', 'lost', 'unknown'].forEach( 198 (s) => { 199 server.createList('allocation', 5, { clientStatus: s }); 200 } 201 ); 202 await Allocations.visit({ id: job.id }); 203 }, 204 filter: (alloc, selection) => 205 alloc.jobId == job.id && selection.includes(alloc.clientStatus), 206 }); 207 208 testFacet('Client', { 209 facet: Allocations.facets.client, 210 paramName: 'client', 211 expectedOptions(allocs) { 212 return Array.from( 213 new Set( 214 allocs 215 .filter((alloc) => alloc.jobId == job.id) 216 .mapBy('nodeId') 217 .map((id) => id.split('-')[0]) 218 ) 219 ).sort(); 220 }, 221 async beforeEach() { 222 server.createList('node', 5); 223 server.createList('allocation', 20); 224 225 await Allocations.visit({ id: job.id }); 226 }, 227 filter: (alloc, selection) => 228 alloc.jobId == job.id && selection.includes(alloc.nodeId.split('-')[0]), 229 }); 230 231 testFacet('Task Group', { 232 facet: Allocations.facets.taskGroup, 233 paramName: 'taskGroup', 234 expectedOptions(allocs) { 235 return Array.from( 236 new Set( 237 allocs.filter((alloc) => alloc.jobId == job.id).mapBy('taskGroup') 238 ) 239 ).sort(); 240 }, 241 async beforeEach() { 242 server.create('node-pool'); 243 job = server.create('job', { 244 type: 'service', 245 status: 'running', 246 groupsCount: 5, 247 }); 248 249 await Allocations.visit({ id: job.id }); 250 }, 251 filter: (alloc, selection) => 252 alloc.jobId == job.id && selection.includes(alloc.taskGroup), 253 }); 254 }); 255 256 function testFacet( 257 label, 258 { facet, paramName, beforeEach, filter, expectedOptions } 259 ) { 260 test(`facet ${label} | the ${label} facet has the correct options`, async function (assert) { 261 await beforeEach(); 262 await facet.toggle(); 263 264 let expectation; 265 if (typeof expectedOptions === 'function') { 266 expectation = expectedOptions(server.db.allocations); 267 } else { 268 expectation = expectedOptions; 269 } 270 271 assert.deepEqual( 272 facet.options.map((option) => option.label.trim()), 273 expectation, 274 'Options for facet are as expected' 275 ); 276 }); 277 278 test(`facet ${label} | the ${label} facet filters the allocations list by ${label}`, async function (assert) { 279 let option; 280 281 await beforeEach(); 282 283 await facet.toggle(); 284 option = facet.options.objectAt(0); 285 await option.toggle(); 286 287 const selection = [option.key]; 288 const expectedAllocs = server.db.allocations 289 .filter((alloc) => filter(alloc, selection)) 290 .sortBy('modifyIndex') 291 .reverse(); 292 293 Allocations.allocations.forEach((alloc, index) => { 294 assert.equal( 295 alloc.id, 296 expectedAllocs[index].id, 297 `Allocation at ${index} is ${expectedAllocs[index].id}` 298 ); 299 }); 300 }); 301 302 test(`facet ${label} | selecting multiple options in the ${label} facet results in a broader search`, async function (assert) { 303 const selection = []; 304 305 await beforeEach(); 306 await facet.toggle(); 307 308 const option1 = facet.options.objectAt(0); 309 const option2 = facet.options.objectAt(1); 310 await option1.toggle(); 311 selection.push(option1.key); 312 await option2.toggle(); 313 selection.push(option2.key); 314 315 const expectedAllocs = server.db.allocations 316 .filter((alloc) => filter(alloc, selection)) 317 .sortBy('modifyIndex') 318 .reverse(); 319 320 Allocations.allocations.forEach((alloc, index) => { 321 assert.equal( 322 alloc.id, 323 expectedAllocs[index].id, 324 `Allocation at ${index} is ${expectedAllocs[index].id}` 325 ); 326 }); 327 }); 328 329 test(`facet ${label} | selecting options in the ${label} facet updates the ${paramName} query param`, async function (assert) { 330 const selection = []; 331 332 await beforeEach(); 333 await facet.toggle(); 334 335 const option1 = facet.options.objectAt(0); 336 const option2 = facet.options.objectAt(1); 337 await option1.toggle(); 338 selection.push(option1.key); 339 await option2.toggle(); 340 selection.push(option2.key); 341 342 assert.equal( 343 currentURL(), 344 `/jobs/${job.id}/allocations?${paramName}=${encodeURIComponent( 345 JSON.stringify(selection) 346 )}`, 347 'URL has the correct query param key and value' 348 ); 349 }); 350 }