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  });