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