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