github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/clients-list-test.js (about) 1 import { currentURL, settled } 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 ClientsList from 'nomad-ui/tests/pages/clients/list'; 8 9 module('Acceptance | clients list', function(hooks) { 10 setupApplicationTest(hooks); 11 setupMirage(hooks); 12 13 hooks.beforeEach(function() { 14 window.localStorage.clear(); 15 }); 16 17 test('it passes an accessibility audit', async function(assert) { 18 const nodesCount = ClientsList.pageSize + 1; 19 20 server.createList('node', nodesCount); 21 server.createList('agent', 1); 22 23 await ClientsList.visit(); 24 await a11yAudit(assert); 25 }); 26 27 test('/clients should list one page of clients', async function(assert) { 28 // Make sure to make more nodes than 1 page to assert that pagination is working 29 const nodesCount = ClientsList.pageSize + 1; 30 31 server.createList('node', nodesCount); 32 server.createList('agent', 1); 33 34 await ClientsList.visit(); 35 36 assert.equal(ClientsList.nodes.length, ClientsList.pageSize); 37 assert.ok(ClientsList.hasPagination, 'Pagination found on the page'); 38 39 const sortedNodes = server.db.nodes.sortBy('modifyIndex').reverse(); 40 41 ClientsList.nodes.forEach((node, index) => { 42 assert.equal(node.id, sortedNodes[index].id.split('-')[0], 'Clients are ordered'); 43 }); 44 45 assert.equal(document.title, 'Clients - Nomad'); 46 }); 47 48 test('each client record should show high-level info of the client', async function(assert) { 49 const node = server.create('node', 'draining', { 50 status: 'ready', 51 }); 52 53 server.createList('agent', 1); 54 55 await ClientsList.visit(); 56 57 const nodeRow = ClientsList.nodes.objectAt(0); 58 const allocations = server.db.allocations.where({ nodeId: node.id }); 59 60 assert.equal(nodeRow.id, node.id.split('-')[0], 'ID'); 61 assert.equal(nodeRow.name, node.name, 'Name'); 62 assert.equal( 63 nodeRow.compositeStatus.text, 64 'draining', 65 'Combined status, draining, and eligbility' 66 ); 67 assert.equal(nodeRow.address, node.httpAddr); 68 assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter'); 69 assert.equal(nodeRow.allocations, allocations.length, '# Allocations'); 70 }); 71 72 test('each client record should show running allocations', async function(assert) { 73 server.createList('agent', 1); 74 75 const node = server.create('node', { 76 modifyIndex: 4, 77 status: 'ready', 78 schedulingEligibility: 'eligible', 79 drain: false, 80 }); 81 82 server.create('job', { createAllocations: false }); 83 84 const running = server.createList('allocation', 2, { clientStatus: 'running' }); 85 server.createList('allocation', 3, { clientStatus: 'pending' }); 86 server.createList('allocation', 10, { clientStatus: 'complete' }); 87 88 await ClientsList.visit(); 89 90 const nodeRow = ClientsList.nodes.objectAt(0); 91 92 assert.equal(nodeRow.id, node.id.split('-')[0], 'ID'); 93 assert.equal( 94 nodeRow.compositeStatus.text, 95 'ready', 96 'Combined status, draining, and eligbility' 97 ); 98 assert.equal(nodeRow.allocations, running.length, '# Allocations'); 99 }); 100 101 test('client status, draining, and eligibility are collapsed into one column that stays sorted', async function(assert) { 102 server.createList('agent', 1); 103 104 server.create('node', { 105 modifyIndex: 5, 106 status: 'ready', 107 schedulingEligibility: 'eligible', 108 drain: false, 109 }); 110 server.create('node', { 111 modifyIndex: 4, 112 status: 'initializing', 113 schedulingEligibility: 'eligible', 114 drain: false, 115 }); 116 server.create('node', { 117 modifyIndex: 3, 118 status: 'down', 119 schedulingEligibility: 'eligible', 120 drain: false, 121 }); 122 server.create('node', { 123 modifyIndex: 2, 124 status: 'down', 125 schedulingEligibility: 'ineligible', 126 drain: false, 127 }); 128 server.create('node', { 129 modifyIndex: 1, 130 status: 'ready', 131 schedulingEligibility: 'ineligible', 132 drain: false, 133 }); 134 server.create('node', 'draining', { 135 modifyIndex: 0, 136 status: 'ready', 137 }); 138 139 await ClientsList.visit(); 140 141 ClientsList.nodes[0].compositeStatus.as(readyClient => { 142 assert.equal(readyClient.text, 'ready'); 143 assert.ok(readyClient.isUnformatted, 'expected no status class'); 144 assert.equal(readyClient.tooltip, 'ready / not draining / eligible'); 145 }); 146 147 assert.equal(ClientsList.nodes[1].compositeStatus.text, 'initializing'); 148 assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down'); 149 assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down', 'down takes priority over ineligible'); 150 151 assert.equal(ClientsList.nodes[4].compositeStatus.text, 'ineligible'); 152 assert.ok(ClientsList.nodes[4].compositeStatus.isWarning, 'expected warning class'); 153 154 assert.equal(ClientsList.nodes[5].compositeStatus.text, 'draining'); 155 assert.ok(ClientsList.nodes[5].compositeStatus.isInfo, 'expected info class'); 156 157 await ClientsList.sortBy('compositeStatus'); 158 159 assert.deepEqual(ClientsList.nodes.mapBy('compositeStatus.text'), [ 160 'ready', 161 'initializing', 162 'ineligible', 163 'draining', 164 'down', 165 'down', 166 ]); 167 168 // Simulate a client state change arriving through polling 169 let readyClient = this.owner 170 .lookup('service:store') 171 .peekAll('node') 172 .findBy('modifyIndex', 5); 173 readyClient.set('schedulingEligibility', 'ineligible'); 174 175 await settled(); 176 177 assert.deepEqual(ClientsList.nodes.mapBy('compositeStatus.text'), [ 178 'initializing', 179 'ineligible', 180 'ineligible', 181 'draining', 182 'down', 183 'down', 184 ]); 185 }); 186 187 test('each client should link to the client detail page', async function(assert) { 188 server.createList('node', 1); 189 server.createList('agent', 1); 190 191 const node = server.db.nodes[0]; 192 193 await ClientsList.visit(); 194 await ClientsList.nodes.objectAt(0).clickRow(); 195 196 assert.equal(currentURL(), `/clients/${node.id}`); 197 }); 198 199 test('when there are no clients, there is an empty message', async function(assert) { 200 server.createList('agent', 1); 201 202 await ClientsList.visit(); 203 204 assert.ok(ClientsList.isEmpty); 205 assert.equal(ClientsList.empty.headline, 'No Clients'); 206 }); 207 208 test('when there are clients, but no matches for a search term, there is an empty message', async function(assert) { 209 server.createList('agent', 1); 210 server.create('node', { name: 'node' }); 211 212 await ClientsList.visit(); 213 214 await ClientsList.search('client'); 215 assert.ok(ClientsList.isEmpty); 216 assert.equal(ClientsList.empty.headline, 'No Matches'); 217 }); 218 219 test('when accessing clients is forbidden, show a message with a link to the tokens page', async function(assert) { 220 server.create('agent'); 221 server.create('node', { name: 'node' }); 222 server.pretender.get('/v1/nodes', () => [403, {}, null]); 223 224 await ClientsList.visit(); 225 226 assert.equal(ClientsList.error.title, 'Not Authorized'); 227 228 await ClientsList.error.seekHelp(); 229 230 assert.equal(currentURL(), '/settings/tokens'); 231 }); 232 233 pageSizeSelect({ 234 resourceName: 'client', 235 pageObject: ClientsList, 236 pageObjectList: ClientsList.nodes, 237 async setup() { 238 server.createList('node', ClientsList.pageSize); 239 server.createList('agent', 1); 240 await ClientsList.visit(); 241 }, 242 }); 243 244 testFacet('Class', { 245 facet: ClientsList.facets.class, 246 paramName: 'class', 247 expectedOptions(nodes) { 248 return Array.from(new Set(nodes.mapBy('nodeClass'))).sort(); 249 }, 250 async beforeEach() { 251 server.create('agent'); 252 server.createList('node', 2, { nodeClass: 'nc-one' }); 253 server.createList('node', 2, { nodeClass: 'nc-two' }); 254 server.createList('node', 2, { nodeClass: 'nc-three' }); 255 await ClientsList.visit(); 256 }, 257 filter: (node, selection) => selection.includes(node.nodeClass), 258 }); 259 260 testFacet('State', { 261 facet: ClientsList.facets.state, 262 paramName: 'state', 263 expectedOptions: ['Initializing', 'Ready', 'Down', 'Ineligible', 'Draining'], 264 async beforeEach() { 265 server.create('agent'); 266 267 server.createList('node', 2, { status: 'initializing' }); 268 server.createList('node', 2, { status: 'ready' }); 269 server.createList('node', 2, { status: 'down' }); 270 271 server.createList('node', 2, { schedulingEligibility: 'eligible', drain: false }); 272 server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: false }); 273 server.createList('node', 2, { schedulingEligibility: 'ineligible', drain: true }); 274 275 await ClientsList.visit(); 276 }, 277 filter: (node, selection) => { 278 if (selection.includes('draining') && !node.drain) return false; 279 if (selection.includes('ineligible') && node.schedulingEligibility === 'eligible') 280 return false; 281 282 return selection.includes(node.status); 283 }, 284 }); 285 286 testFacet('Datacenters', { 287 facet: ClientsList.facets.datacenter, 288 paramName: 'dc', 289 expectedOptions(nodes) { 290 return Array.from(new Set(nodes.mapBy('datacenter'))).sort(); 291 }, 292 async beforeEach() { 293 server.create('agent'); 294 server.createList('node', 2, { datacenter: 'pdx-1' }); 295 server.createList('node', 2, { datacenter: 'nyc-1' }); 296 server.createList('node', 2, { datacenter: 'ams-1' }); 297 await ClientsList.visit(); 298 }, 299 filter: (node, selection) => selection.includes(node.datacenter), 300 }); 301 302 testFacet('Volumes', { 303 facet: ClientsList.facets.volume, 304 paramName: 'volume', 305 expectedOptions(nodes) { 306 const flatten = (acc, val) => acc.concat(Object.keys(val)); 307 return Array.from(new Set(nodes.mapBy('hostVolumes').reduce(flatten, []))); 308 }, 309 async beforeEach() { 310 server.create('agent'); 311 server.createList('node', 2, { hostVolumes: { One: { Name: 'One' } } }); 312 server.createList('node', 2, { hostVolumes: { One: { Name: 'One' }, Two: { Name: 'Two' } } }); 313 server.createList('node', 2, { hostVolumes: { Two: { Name: 'Two' } } }); 314 await ClientsList.visit(); 315 }, 316 filter: (node, selection) => 317 Object.keys(node.hostVolumes).find(volume => selection.includes(volume)), 318 }); 319 320 test('when the facet selections result in no matches, the empty state states why', async function(assert) { 321 server.create('agent'); 322 server.createList('node', 2, { status: 'ready' }); 323 324 await ClientsList.visit(); 325 326 await ClientsList.facets.state.toggle(); 327 await ClientsList.facets.state.options.objectAt(0).toggle(); 328 assert.ok(ClientsList.isEmpty, 'There is an empty message'); 329 assert.equal(ClientsList.empty.headline, 'No Matches', 'The message is appropriate'); 330 }); 331 332 test('the clients list is immediately filtered based on query params', async function(assert) { 333 server.create('agent'); 334 server.create('node', { nodeClass: 'omg-large' }); 335 server.create('node', { nodeClass: 'wtf-tiny' }); 336 337 await ClientsList.visit({ class: JSON.stringify(['wtf-tiny']) }); 338 339 assert.equal(ClientsList.nodes.length, 1, 'Only one client shown due to query param'); 340 }); 341 342 function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) { 343 test(`the ${label} facet has the correct options`, async function(assert) { 344 await beforeEach(); 345 await facet.toggle(); 346 347 let expectation; 348 if (typeof expectedOptions === 'function') { 349 expectation = expectedOptions(server.db.nodes); 350 } else { 351 expectation = expectedOptions; 352 } 353 354 assert.deepEqual( 355 facet.options.map(option => option.label.trim()), 356 expectation, 357 'Options for facet are as expected' 358 ); 359 }); 360 361 test(`the ${label} facet filters the nodes list by ${label}`, async function(assert) { 362 let option; 363 364 await beforeEach(); 365 366 await facet.toggle(); 367 option = facet.options.objectAt(0); 368 await option.toggle(); 369 370 const selection = [option.key]; 371 const expectedNodes = server.db.nodes 372 .filter(node => filter(node, selection)) 373 .sortBy('modifyIndex') 374 .reverse(); 375 376 ClientsList.nodes.forEach((node, index) => { 377 assert.equal( 378 node.id, 379 expectedNodes[index].id.split('-')[0], 380 `Node at ${index} is ${expectedNodes[index].id}` 381 ); 382 }); 383 }); 384 385 test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) { 386 const selection = []; 387 388 await beforeEach(); 389 await facet.toggle(); 390 391 const option1 = facet.options.objectAt(0); 392 const option2 = facet.options.objectAt(1); 393 await option1.toggle(); 394 selection.push(option1.key); 395 await option2.toggle(); 396 selection.push(option2.key); 397 398 const expectedNodes = server.db.nodes 399 .filter(node => filter(node, selection)) 400 .sortBy('modifyIndex') 401 .reverse(); 402 403 ClientsList.nodes.forEach((node, index) => { 404 assert.equal( 405 node.id, 406 expectedNodes[index].id.split('-')[0], 407 `Node at ${index} is ${expectedNodes[index].id}` 408 ); 409 }); 410 }); 411 412 test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) { 413 const selection = []; 414 415 await beforeEach(); 416 await facet.toggle(); 417 418 const option1 = facet.options.objectAt(0); 419 const option2 = facet.options.objectAt(1); 420 await option1.toggle(); 421 selection.push(option1.key); 422 await option2.toggle(); 423 selection.push(option2.key); 424 425 assert.equal( 426 currentURL(), 427 `/clients?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`, 428 'URL has the correct query param key and value' 429 ); 430 }); 431 } 432 });