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