github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/integration/components/job-editor-test.js (about) 1 import { assign } from '@ember/polyfills'; 2 import { module, test } from 'qunit'; 3 import { setupRenderingTest } from 'ember-qunit'; 4 import hbs from 'htmlbars-inline-precompile'; 5 import { create } from 'ember-cli-page-object'; 6 import sinon from 'sinon'; 7 import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; 8 import jobEditor from 'nomad-ui/tests/pages/components/job-editor'; 9 import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; 10 import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror'; 11 import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; 12 13 const Editor = create(jobEditor()); 14 15 module('Integration | Component | job-editor', function (hooks) { 16 setupRenderingTest(hooks); 17 setupCodeMirror(hooks); 18 19 hooks.beforeEach(async function () { 20 window.localStorage.clear(); 21 22 fragmentSerializerInitializer(this.owner); 23 24 this.store = this.owner.lookup('service:store'); 25 this.server = startMirage(); 26 27 // Required for placing allocations (a result of creating jobs) 28 this.server.create('node'); 29 }); 30 31 hooks.afterEach(async function () { 32 this.server.shutdown(); 33 }); 34 35 const newJobName = 'new-job'; 36 const newJobTaskGroupName = 'redis'; 37 const jsonJob = (overrides) => { 38 return JSON.stringify( 39 assign( 40 {}, 41 { 42 Name: newJobName, 43 Namespace: 'default', 44 Datacenters: ['dc1'], 45 Priority: 50, 46 TaskGroups: [ 47 { 48 Name: newJobTaskGroupName, 49 Tasks: [ 50 { 51 Name: 'redis', 52 Driver: 'docker', 53 }, 54 ], 55 }, 56 ], 57 }, 58 overrides 59 ), 60 null, 61 2 62 ); 63 }; 64 65 const hclJob = () => ` 66 job "${newJobName}" { 67 namespace = "default" 68 datacenters = ["dc1"] 69 70 task "${newJobTaskGroupName}" { 71 driver = "docker" 72 } 73 } 74 `; 75 76 const commonTemplate = hbs` 77 <JobEditor 78 @job={{job}} 79 @context={{context}} 80 @onSubmit={{onSubmit}} /> 81 `; 82 83 const cancelableTemplate = hbs` 84 <JobEditor 85 @job={{job}} 86 @context={{context}} 87 @cancelable={{true}} 88 @onSubmit={{onSubmit}} 89 @onCancel={{onCancel}} /> 90 `; 91 92 const renderNewJob = async (component, job) => { 93 component.setProperties({ job, onSubmit: sinon.spy(), context: 'new' }); 94 await component.render(commonTemplate); 95 }; 96 97 const renderEditJob = async (component, job) => { 98 component.setProperties({ 99 job, 100 onSubmit: sinon.spy(), 101 onCancel: sinon.spy(), 102 context: 'edit', 103 }); 104 await component.render(cancelableTemplate); 105 }; 106 107 const planJob = async (spec) => { 108 const cm = getCodeMirrorInstance(['data-test-editor']); 109 cm.setValue(spec); 110 await Editor.plan(); 111 }; 112 113 test('the default state is an editor with an explanation popup', async function (assert) { 114 assert.expect(2); 115 116 const job = await this.store.createRecord('job'); 117 118 await renderNewJob(this, job); 119 assert.ok(Editor.editor.isPresent, 'Editor is present'); 120 121 await componentA11yAudit(this.element, assert); 122 }); 123 124 test('submitting a json job skips the parse endpoint', async function (assert) { 125 const spec = jsonJob(); 126 const job = await this.store.createRecord('job'); 127 128 await renderNewJob(this, job); 129 await planJob(spec); 130 console.log('wait'); 131 const requests = this.server.pretender.handledRequests.mapBy('url'); 132 assert.notOk( 133 requests.includes('/v1/jobs/parse'), 134 'JSON job spec is not parsed' 135 ); 136 assert.ok( 137 requests.includes(`/v1/job/${newJobName}/plan`), 138 'JSON job spec is still planned' 139 ); 140 }); 141 142 test('submitting an hcl job requires the parse endpoint', async function (assert) { 143 const spec = hclJob(); 144 const job = await this.store.createRecord('job'); 145 146 await renderNewJob(this, job); 147 await planJob(spec); 148 const requests = this.server.pretender.handledRequests.mapBy('url'); 149 assert.ok( 150 requests.includes('/v1/jobs/parse?namespace=*'), 151 'HCL job spec is parsed first' 152 ); 153 assert.ok( 154 requests.includes(`/v1/job/${newJobName}/plan`), 155 'HCL job spec is planned' 156 ); 157 assert.ok( 158 requests.indexOf('/v1/jobs/parse') < 159 requests.indexOf(`/v1/job/${newJobName}/plan`), 160 'Parse comes before Plan' 161 ); 162 }); 163 164 test('when a job is successfully parsed and planned, the plan is shown to the user', async function (assert) { 165 assert.expect(4); 166 167 const spec = hclJob(); 168 const job = await this.store.createRecord('job'); 169 170 await renderNewJob(this, job); 171 await planJob(spec); 172 assert.ok(Editor.planOutput, 'The plan is outputted'); 173 assert.notOk( 174 Editor.editor.isPresent, 175 'The editor is replaced with the plan output' 176 ); 177 assert.ok(Editor.planHelp.isPresent, 'The plan explanation popup is shown'); 178 179 await componentA11yAudit(this.element, assert); 180 }); 181 182 test('from the plan screen, the cancel button goes back to the editor with the job still in tact', async function (assert) { 183 const spec = hclJob(); 184 const job = await this.store.createRecord('job'); 185 186 await renderNewJob(this, job); 187 await planJob(spec); 188 await Editor.cancel(); 189 assert.ok(Editor.editor.isPresent, 'The editor is shown again'); 190 assert.equal( 191 Editor.editor.contents, 192 spec, 193 'The spec that was planned is still in the editor' 194 ); 195 }); 196 197 test('when parse fails, the parse error message is shown', async function (assert) { 198 assert.expect(5); 199 200 const spec = hclJob(); 201 const errorMessage = 'Parse Failed!! :o'; 202 const job = await this.store.createRecord('job'); 203 204 this.server.pretender.post('/v1/jobs/parse', () => [400, {}, errorMessage]); 205 206 await renderNewJob(this, job); 207 await planJob(spec); 208 assert.notOk(Editor.planError.isPresent, 'Plan error is not shown'); 209 assert.notOk(Editor.runError.isPresent, 'Run error is not shown'); 210 211 assert.ok(Editor.parseError.isPresent, 'Parse error is shown'); 212 assert.equal( 213 Editor.parseError.message, 214 errorMessage, 215 'The error message from the server is shown in the error in the UI' 216 ); 217 218 await componentA11yAudit(this.element, assert); 219 }); 220 221 test('when plan fails, the plan error message is shown', async function (assert) { 222 assert.expect(5); 223 224 const spec = hclJob(); 225 const errorMessage = 'Plan Failed!! :o'; 226 const job = await this.store.createRecord('job'); 227 228 this.server.pretender.post(`/v1/job/${newJobName}/plan`, () => [ 229 400, 230 {}, 231 errorMessage, 232 ]); 233 234 await renderNewJob(this, job); 235 await planJob(spec); 236 assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown'); 237 assert.notOk(Editor.runError.isPresent, 'Run error is not shown'); 238 239 assert.ok(Editor.planError.isPresent, 'Plan error is shown'); 240 assert.equal( 241 Editor.planError.message, 242 errorMessage, 243 'The error message from the server is shown in the error in the UI' 244 ); 245 246 await componentA11yAudit(this.element, assert); 247 }); 248 249 test('when run fails, the run error message is shown', async function (assert) { 250 assert.expect(5); 251 252 const spec = hclJob(); 253 const errorMessage = 'Run Failed!! :o'; 254 const job = await this.store.createRecord('job'); 255 256 this.server.pretender.post('/v1/jobs', () => [400, {}, errorMessage]); 257 258 await renderNewJob(this, job); 259 await planJob(spec); 260 await Editor.run(); 261 assert.notOk(Editor.planError.isPresent, 'Plan error is not shown'); 262 assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown'); 263 264 assert.ok(Editor.runError.isPresent, 'Run error is shown'); 265 assert.equal( 266 Editor.runError.message, 267 errorMessage, 268 'The error message from the server is shown in the error in the UI' 269 ); 270 271 await componentA11yAudit(this.element, assert); 272 }); 273 274 test('when the scheduler dry-run has warnings, the warnings are shown to the user', async function (assert) { 275 assert.expect(4); 276 277 const spec = jsonJob({ Unschedulable: true }); 278 const job = await this.store.createRecord('job'); 279 280 await renderNewJob(this, job); 281 await planJob(spec); 282 assert.ok( 283 Editor.dryRunMessage.errored, 284 'The scheduler dry-run message is in the warning state' 285 ); 286 assert.notOk( 287 Editor.dryRunMessage.succeeded, 288 'The success message is not shown in addition to the warning message' 289 ); 290 assert.ok( 291 Editor.dryRunMessage.body.includes(newJobTaskGroupName), 292 'The scheduler dry-run message includes the warning from send back by the API' 293 ); 294 295 await componentA11yAudit(this.element, assert); 296 }); 297 298 test('when the scheduler dry-run has no warnings, a success message is shown to the user', async function (assert) { 299 assert.expect(3); 300 301 const spec = hclJob(); 302 const job = await this.store.createRecord('job'); 303 304 await renderNewJob(this, job); 305 await planJob(spec); 306 assert.ok( 307 Editor.dryRunMessage.succeeded, 308 'The scheduler dry-run message is in the success state' 309 ); 310 assert.notOk( 311 Editor.dryRunMessage.errored, 312 'The warning message is not shown in addition to the success message' 313 ); 314 315 await componentA11yAudit(this.element, assert); 316 }); 317 318 test('when a job is submitted in the edit context, a POST request is made to the update job endpoint', async function (assert) { 319 const spec = hclJob(); 320 const job = await this.store.createRecord('job'); 321 322 await renderEditJob(this, job); 323 await planJob(spec); 324 await Editor.run(); 325 const requests = this.server.pretender.handledRequests 326 .filterBy('method', 'POST') 327 .mapBy('url'); 328 assert.ok( 329 requests.includes(`/v1/job/${newJobName}`), 330 'A request was made to job update' 331 ); 332 assert.notOk( 333 requests.includes('/v1/jobs'), 334 'A request was not made to job create' 335 ); 336 }); 337 338 test('when a job is submitted in the new context, a POST request is made to the create job endpoint', async function (assert) { 339 const spec = hclJob(); 340 const job = await this.store.createRecord('job'); 341 342 await renderNewJob(this, job); 343 await planJob(spec); 344 await Editor.run(); 345 const requests = this.server.pretender.handledRequests 346 .filterBy('method', 'POST') 347 .mapBy('url'); 348 assert.ok( 349 requests.includes('/v1/jobs'), 350 'A request was made to job create' 351 ); 352 assert.notOk( 353 requests.includes(`/v1/job/${newJobName}`), 354 'A request was not made to job update' 355 ); 356 }); 357 358 test('when a job is successfully submitted, the onSubmit hook is called', async function (assert) { 359 const spec = hclJob(); 360 const job = await this.store.createRecord('job'); 361 362 await renderNewJob(this, job); 363 await planJob(spec); 364 await Editor.run(); 365 assert.ok( 366 this.onSubmit.calledWith(newJobName, 'default'), 367 'The onSubmit hook was called with the correct arguments' 368 ); 369 }); 370 371 test('when the job-editor cancelable flag is false, there is no cancel button in the header', async function (assert) { 372 const job = await this.store.createRecord('job'); 373 374 await renderNewJob(this, job); 375 assert.notOk(Editor.cancelEditingIsAvailable, 'No way to cancel editing'); 376 }); 377 378 test('when the job-editor cancelable flag is true, there is a cancel button in the header', async function (assert) { 379 assert.expect(2); 380 381 const job = await this.store.createRecord('job'); 382 383 await renderEditJob(this, job); 384 assert.ok(Editor.cancelEditingIsAvailable, 'Cancel editing button exists'); 385 386 await componentA11yAudit(this.element, assert); 387 }); 388 389 test('when the job-editor cancel button is clicked, the onCancel hook is called', async function (assert) { 390 const job = await this.store.createRecord('job'); 391 392 await renderEditJob(this, job); 393 await Editor.cancelEditing(); 394 assert.ok(this.onCancel.calledOnce, 'The onCancel hook was called'); 395 }); 396 });