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