github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/volumes-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, visit } 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 VolumesList from 'nomad-ui/tests/pages/storage/volumes/list'; 14 import percySnapshot from '@percy/ember'; 15 import faker from 'nomad-ui/mirage/faker'; 16 17 const assignWriteAlloc = (volume, alloc) => { 18 volume.writeAllocs.add(alloc); 19 volume.allocations.add(alloc); 20 volume.save(); 21 }; 22 23 const assignReadAlloc = (volume, alloc) => { 24 volume.readAllocs.add(alloc); 25 volume.allocations.add(alloc); 26 volume.save(); 27 }; 28 29 module('Acceptance | volumes list', function (hooks) { 30 setupApplicationTest(hooks); 31 setupMirage(hooks); 32 33 hooks.beforeEach(function () { 34 faker.seed(1); 35 server.create('node-pool'); 36 server.create('node'); 37 server.create('csi-plugin', { createVolumes: false }); 38 window.localStorage.clear(); 39 }); 40 41 test('it passes an accessibility audit', async function (assert) { 42 await VolumesList.visit(); 43 await a11yAudit(assert); 44 }); 45 46 test('visiting /csi redirects to /csi/volumes', async function (assert) { 47 await visit('/csi'); 48 49 assert.equal(currentURL(), '/csi/volumes'); 50 }); 51 52 test('visiting /csi/volumes', async function (assert) { 53 await VolumesList.visit(); 54 55 assert.equal(currentURL(), '/csi/volumes'); 56 assert.equal(document.title, 'CSI Volumes - Nomad'); 57 }); 58 59 test('/csi/volumes should list the first page of volumes sorted by name', async function (assert) { 60 const volumeCount = VolumesList.pageSize + 1; 61 server.createList('csi-volume', volumeCount); 62 63 await VolumesList.visit(); 64 65 await percySnapshot(assert); 66 67 const sortedVolumes = server.db.csiVolumes.sortBy('id'); 68 assert.equal(VolumesList.volumes.length, VolumesList.pageSize); 69 VolumesList.volumes.forEach((volume, index) => { 70 assert.equal(volume.name, sortedVolumes[index].id, 'Volumes are ordered'); 71 }); 72 }); 73 74 test('each volume row should contain information about the volume', async function (assert) { 75 const volume = server.create('csi-volume'); 76 const readAllocs = server.createList('allocation', 2, { shallow: true }); 77 const writeAllocs = server.createList('allocation', 3, { shallow: true }); 78 readAllocs.forEach((alloc) => assignReadAlloc(volume, alloc)); 79 writeAllocs.forEach((alloc) => assignWriteAlloc(volume, alloc)); 80 81 await VolumesList.visit(); 82 83 const volumeRow = VolumesList.volumes.objectAt(0); 84 85 let controllerHealthStr = 'Node Only'; 86 if (volume.controllerRequired || volume.controllersExpected > 0) { 87 const healthy = volume.controllersHealthy; 88 const expected = volume.controllersExpected; 89 const isHealthy = healthy > 0; 90 controllerHealthStr = `${ 91 isHealthy ? 'Healthy' : 'Unhealthy' 92 } ( ${healthy} / ${expected} )`; 93 } 94 95 const nodeHealthStr = volume.nodesHealthy > 0 ? 'Healthy' : 'Unhealthy'; 96 97 assert.equal(volumeRow.name, volume.id); 98 assert.notOk(volumeRow.hasNamespace); 99 assert.equal( 100 volumeRow.schedulable, 101 volume.schedulable ? 'Schedulable' : 'Unschedulable' 102 ); 103 assert.equal(volumeRow.controllerHealth, controllerHealthStr); 104 assert.equal( 105 volumeRow.nodeHealth, 106 `${nodeHealthStr} ( ${volume.nodesHealthy} / ${volume.nodesExpected} )` 107 ); 108 assert.equal(volumeRow.provider, volume.provider); 109 assert.equal(volumeRow.allocations, readAllocs.length + writeAllocs.length); 110 }); 111 112 test('each volume row should link to the corresponding volume', async function (assert) { 113 const [, secondNamespace] = server.createList('namespace', 2); 114 const volume = server.create('csi-volume', { 115 namespaceId: secondNamespace.id, 116 }); 117 118 await VolumesList.visit({ namespace: '*' }); 119 120 await VolumesList.volumes.objectAt(0).clickName(); 121 assert.equal( 122 currentURL(), 123 `/csi/volumes/${volume.id}@${secondNamespace.id}` 124 ); 125 126 await VolumesList.visit({ namespace: '*' }); 127 assert.equal(currentURL(), '/csi/volumes?namespace=*'); 128 129 await VolumesList.volumes.objectAt(0).clickRow(); 130 assert.equal( 131 currentURL(), 132 `/csi/volumes/${volume.id}@${secondNamespace.id}` 133 ); 134 }); 135 136 test('when there are no volumes, there is an empty message', async function (assert) { 137 await VolumesList.visit(); 138 139 await percySnapshot(assert); 140 141 assert.ok(VolumesList.isEmpty); 142 assert.equal(VolumesList.emptyState.headline, 'No Volumes'); 143 }); 144 145 test('when there are volumes, but no matches for a search, there is an empty message', async function (assert) { 146 server.create('csi-volume', { id: 'cat 1' }); 147 server.create('csi-volume', { id: 'cat 2' }); 148 149 await VolumesList.visit(); 150 151 await VolumesList.search('dog'); 152 assert.ok(VolumesList.isEmpty); 153 assert.equal(VolumesList.emptyState.headline, 'No Matches'); 154 }); 155 156 test('searching resets the current page', async function (assert) { 157 server.createList('csi-volume', VolumesList.pageSize + 1); 158 159 await VolumesList.visit(); 160 await VolumesList.nextPage(); 161 162 assert.equal(currentURL(), '/csi/volumes?page=2'); 163 164 await VolumesList.search('foobar'); 165 166 assert.equal(currentURL(), '/csi/volumes?search=foobar'); 167 }); 168 169 test('when the cluster has namespaces, each volume row includes the volume namespace', async function (assert) { 170 server.createList('namespace', 2); 171 const volume = server.create('csi-volume'); 172 173 await VolumesList.visit({ namespace: '*' }); 174 175 const volumeRow = VolumesList.volumes.objectAt(0); 176 assert.equal(volumeRow.namespace, volume.namespaceId); 177 }); 178 179 test('when the namespace query param is set, only matching volumes are shown and the namespace value is forwarded to app state', async function (assert) { 180 server.createList('namespace', 2); 181 const volume1 = server.create('csi-volume', { 182 namespaceId: server.db.namespaces[0].id, 183 }); 184 const volume2 = server.create('csi-volume', { 185 namespaceId: server.db.namespaces[1].id, 186 }); 187 188 await VolumesList.visit(); 189 assert.equal(VolumesList.volumes.length, 2); 190 191 const firstNamespace = server.db.namespaces[0]; 192 await VolumesList.visit({ namespace: firstNamespace.id }); 193 assert.equal(VolumesList.volumes.length, 1); 194 assert.equal(VolumesList.volumes.objectAt(0).name, volume1.id); 195 196 const secondNamespace = server.db.namespaces[1]; 197 await VolumesList.visit({ namespace: secondNamespace.id }); 198 199 assert.equal(VolumesList.volumes.length, 1); 200 assert.equal(VolumesList.volumes.objectAt(0).name, volume2.id); 201 }); 202 203 test('when accessing volumes is forbidden, a message is shown with a link to the tokens page', async function (assert) { 204 server.pretender.get('/v1/volumes', () => [403, {}, null]); 205 206 await VolumesList.visit(); 207 assert.equal(VolumesList.error.title, 'Not Authorized'); 208 209 await VolumesList.error.seekHelp(); 210 assert.equal(currentURL(), '/settings/tokens'); 211 }); 212 213 pageSizeSelect({ 214 resourceName: 'volume', 215 pageObject: VolumesList, 216 pageObjectList: VolumesList.volumes, 217 async setup() { 218 server.createList('csi-volume', VolumesList.pageSize); 219 await VolumesList.visit(); 220 }, 221 }); 222 223 testSingleSelectFacet('Namespace', { 224 facet: VolumesList.facets.namespace, 225 paramName: 'namespace', 226 expectedOptions: ['All (*)', 'default', 'namespace-2'], 227 optionToSelect: 'namespace-2', 228 async beforeEach() { 229 server.create('namespace', { id: 'default' }); 230 server.create('namespace', { id: 'namespace-2' }); 231 server.createList('csi-volume', 2, { namespaceId: 'default' }); 232 server.createList('csi-volume', 2, { namespaceId: 'namespace-2' }); 233 await VolumesList.visit(); 234 }, 235 filter(volume, selection) { 236 return volume.namespaceId === selection; 237 }, 238 }); 239 240 function testSingleSelectFacet( 241 label, 242 { facet, paramName, beforeEach, filter, expectedOptions, optionToSelect } 243 ) { 244 test(`the ${label} facet has the correct options`, async function (assert) { 245 await beforeEach(); 246 await facet.toggle(); 247 248 let expectation; 249 if (typeof expectedOptions === 'function') { 250 expectation = expectedOptions(server.db.jobs); 251 } else { 252 expectation = expectedOptions; 253 } 254 255 assert.deepEqual( 256 facet.options.map((option) => option.label.trim()), 257 expectation, 258 'Options for facet are as expected' 259 ); 260 }); 261 262 test(`the ${label} facet filters the volumes list by ${label}`, async function (assert) { 263 await beforeEach(); 264 await facet.toggle(); 265 266 const option = facet.options.findOneBy('label', optionToSelect); 267 const selection = option.key; 268 await option.select(); 269 270 const expectedVolumes = server.db.csiVolumes 271 .filter((volume) => filter(volume, selection)) 272 .sortBy('id'); 273 274 VolumesList.volumes.forEach((volume, index) => { 275 assert.equal( 276 volume.name, 277 expectedVolumes[index].name, 278 `Volume at ${index} is ${expectedVolumes[index].name}` 279 ); 280 }); 281 }); 282 283 test(`selecting an option in the ${label} facet updates the ${paramName} query param`, async function (assert) { 284 await beforeEach(); 285 await facet.toggle(); 286 287 const option = facet.options.objectAt(1); 288 const selection = option.key; 289 await option.select(); 290 291 assert.ok( 292 currentURL().includes(`${paramName}=${selection}`), 293 'URL has the correct query param key and value' 294 ); 295 }); 296 } 297 });