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