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