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