github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/tests/acceptance/jobs-list-test.js (about)

     1  import { currentURL } from '@ember/test-helpers';
     2  import { module, test } from 'qunit';
     3  import { setupApplicationTest } from 'ember-qunit';
     4  import { setupMirage } from 'ember-cli-mirage/test-support';
     5  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
     6  import pageSizeSelect from './behaviors/page-size-select';
     7  import JobsList from 'nomad-ui/tests/pages/jobs/list';
     8  import Layout from 'nomad-ui/tests/pages/layout';
     9  
    10  let managementToken, clientToken;
    11  
    12  module('Acceptance | jobs list', function(hooks) {
    13    setupApplicationTest(hooks);
    14    setupMirage(hooks);
    15  
    16    hooks.beforeEach(function() {
    17      // Required for placing allocations (a result of creating jobs)
    18      server.create('node');
    19  
    20      managementToken = server.create('token');
    21      clientToken = server.create('token');
    22  
    23      window.localStorage.clear();
    24      window.localStorage.nomadTokenSecret = managementToken.secretId;
    25    });
    26  
    27    test('it passes an accessibility audit', async function(assert) {
    28      await JobsList.visit();
    29      await a11yAudit(assert);
    30    });
    31  
    32    test('visiting /jobs', async function(assert) {
    33      await JobsList.visit();
    34  
    35      assert.equal(currentURL(), '/jobs');
    36      assert.equal(document.title, 'Jobs - Nomad');
    37    });
    38  
    39    test('/jobs should list the first page of jobs sorted by modify index', async function(assert) {
    40      const jobsCount = JobsList.pageSize + 1;
    41      server.createList('job', jobsCount, { createAllocations: false });
    42  
    43      await JobsList.visit();
    44  
    45      const sortedJobs = server.db.jobs.sortBy('modifyIndex').reverse();
    46      assert.equal(JobsList.jobs.length, JobsList.pageSize);
    47      JobsList.jobs.forEach((job, index) => {
    48        assert.equal(job.name, sortedJobs[index].name, 'Jobs are ordered');
    49      });
    50    });
    51  
    52    test('each job row should contain information about the job', async function(assert) {
    53      server.createList('job', 2);
    54      const job = server.db.jobs.sortBy('modifyIndex').reverse()[0];
    55      const taskGroups = server.db.taskGroups.where({ jobId: job.id });
    56  
    57      await JobsList.visit();
    58  
    59      const jobRow = JobsList.jobs.objectAt(0);
    60  
    61      assert.equal(jobRow.name, job.name, 'Name');
    62      assert.equal(jobRow.link, `/ui/jobs/${job.id}`, 'Detail Link');
    63      assert.equal(jobRow.status, job.status, 'Status');
    64      assert.equal(jobRow.type, typeForJob(job), 'Type');
    65      assert.equal(jobRow.priority, job.priority, 'Priority');
    66      assert.equal(jobRow.taskGroups, taskGroups.length, '# Groups');
    67    });
    68  
    69    test('each job row should link to the corresponding job', async function(assert) {
    70      server.create('job');
    71      const job = server.db.jobs[0];
    72  
    73      await JobsList.visit();
    74      await JobsList.jobs.objectAt(0).clickName();
    75  
    76      assert.equal(currentURL(), `/jobs/${job.id}`);
    77    });
    78  
    79    test('the new job button transitions to the new job page', async function(assert) {
    80      await JobsList.visit();
    81      await JobsList.runJobButton.click();
    82  
    83      assert.equal(currentURL(), '/jobs/run');
    84    });
    85  
    86    test('the job run button is disabled when the token lacks permission', async function(assert) {
    87      window.localStorage.nomadTokenSecret = clientToken.secretId;
    88      await JobsList.visit();
    89  
    90      assert.ok(JobsList.runJobButton.isDisabled);
    91  
    92      await JobsList.runJobButton.click();
    93      assert.equal(currentURL(), '/jobs');
    94    });
    95  
    96    test('the job run button state can change between namespaces', async function(assert) {
    97      server.createList('namespace', 2);
    98      const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
    99      const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
   100  
   101      window.localStorage.nomadTokenSecret = clientToken.secretId;
   102  
   103      const policy = server.create('policy', {
   104        id: 'something',
   105        name: 'something',
   106        rulesJSON: {
   107          Namespaces: [
   108            {
   109              Name: job1.namespaceId,
   110              Capabilities: ['list-jobs', 'submit-job'],
   111            },
   112            {
   113              Name: job2.namespaceId,
   114              Capabilities: ['list-jobs'],
   115            },
   116          ],
   117        },
   118      });
   119  
   120      clientToken.policyIds = [policy.id];
   121      clientToken.save();
   122  
   123      await JobsList.visit();
   124      assert.notOk(JobsList.runJobButton.isDisabled);
   125  
   126      const secondNamespace = server.db.namespaces[1];
   127      await JobsList.visit({ namespace: secondNamespace.id });
   128      assert.ok(JobsList.runJobButton.isDisabled);
   129    });
   130  
   131    test('the anonymous policy is fetched to check whether to show the job run button', async function(assert) {
   132      window.localStorage.removeItem('nomadTokenSecret');
   133  
   134      server.create('policy', {
   135        id: 'anonymous',
   136        name: 'anonymous',
   137        rulesJSON: {
   138          Namespaces: [
   139            {
   140              Name: 'default',
   141              Capabilities: ['list-jobs', 'submit-job'],
   142            },
   143          ],
   144        },
   145      });
   146  
   147      await JobsList.visit();
   148      assert.notOk(JobsList.runJobButton.isDisabled);
   149    });
   150  
   151    test('when there are no jobs, there is an empty message', async function(assert) {
   152      await JobsList.visit();
   153  
   154      assert.ok(JobsList.isEmpty, 'There is an empty message');
   155      assert.equal(JobsList.emptyState.headline, 'No Jobs', 'The message is appropriate');
   156    });
   157  
   158    test('when there are jobs, but no matches for a search result, there is an empty message', async function(assert) {
   159      server.create('job', { name: 'cat 1' });
   160      server.create('job', { name: 'cat 2' });
   161  
   162      await JobsList.visit();
   163  
   164      await JobsList.search.fillIn('dog');
   165      assert.ok(JobsList.isEmpty, 'The empty message is shown');
   166      assert.equal(JobsList.emptyState.headline, 'No Matches', 'The message is appropriate');
   167    });
   168  
   169    test('searching resets the current page', async function(assert) {
   170      server.createList('job', JobsList.pageSize + 1, { createAllocations: false });
   171  
   172      await JobsList.visit();
   173      await JobsList.nextPage();
   174  
   175      assert.equal(currentURL(), '/jobs?page=2', 'Page query param captures page=2');
   176  
   177      await JobsList.search.fillIn('foobar');
   178  
   179      assert.equal(currentURL(), '/jobs?search=foobar', 'No page query param');
   180    });
   181  
   182    test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', async function(assert) {
   183      server.createList('namespace', 2);
   184      const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
   185      const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
   186  
   187      await JobsList.visit();
   188  
   189      assert.equal(JobsList.jobs.length, 1, 'One job in the default namespace');
   190      assert.equal(JobsList.jobs.objectAt(0).name, job1.name, 'The correct job is shown');
   191  
   192      const secondNamespace = server.db.namespaces[1];
   193      await JobsList.visit({ namespace: secondNamespace.id });
   194  
   195      assert.equal(JobsList.jobs.length, 1, `One job in the ${secondNamespace.name} namespace`);
   196      assert.equal(JobsList.jobs.objectAt(0).name, job2.name, 'The correct job is shown');
   197    });
   198  
   199    test('when accessing jobs is forbidden, show a message with a link to the tokens page', async function(assert) {
   200      server.pretender.get('/v1/jobs', () => [403, {}, null]);
   201  
   202      await JobsList.visit();
   203      assert.equal(JobsList.error.title, 'Not Authorized');
   204  
   205      await JobsList.error.seekHelp();
   206      assert.equal(currentURL(), '/settings/tokens');
   207    });
   208  
   209    function typeForJob(job) {
   210      return job.periodic ? 'periodic' : job.parameterized ? 'parameterized' : job.type;
   211    }
   212  
   213    test('the jobs list page has appropriate faceted search options', async function(assert) {
   214      await JobsList.visit();
   215  
   216      assert.ok(JobsList.facets.type.isPresent, 'Type facet found');
   217      assert.ok(JobsList.facets.status.isPresent, 'Status facet found');
   218      assert.ok(JobsList.facets.datacenter.isPresent, 'Datacenter facet found');
   219      assert.ok(JobsList.facets.prefix.isPresent, 'Prefix facet found');
   220    });
   221  
   222    testFacet('Type', {
   223      facet: JobsList.facets.type,
   224      paramName: 'type',
   225      expectedOptions: ['Batch', 'Parameterized', 'Periodic', 'Service', 'System'],
   226      async beforeEach() {
   227        server.createList('job', 2, { createAllocations: false, type: 'batch' });
   228        server.createList('job', 2, {
   229          createAllocations: false,
   230          type: 'batch',
   231          periodic: true,
   232          childrenCount: 0,
   233        });
   234        server.createList('job', 2, {
   235          createAllocations: false,
   236          type: 'batch',
   237          parameterized: true,
   238          childrenCount: 0,
   239        });
   240        server.createList('job', 2, { createAllocations: false, type: 'service' });
   241        await JobsList.visit();
   242      },
   243      filter(job, selection) {
   244        let displayType = job.type;
   245        if (job.parameterized) displayType = 'parameterized';
   246        if (job.periodic) displayType = 'periodic';
   247        return selection.includes(displayType);
   248      },
   249    });
   250  
   251    testFacet('Status', {
   252      facet: JobsList.facets.status,
   253      paramName: 'status',
   254      expectedOptions: ['Pending', 'Running', 'Dead'],
   255      async beforeEach() {
   256        server.createList('job', 2, {
   257          status: 'pending',
   258          createAllocations: false,
   259          childrenCount: 0,
   260        });
   261        server.createList('job', 2, {
   262          status: 'running',
   263          createAllocations: false,
   264          childrenCount: 0,
   265        });
   266        server.createList('job', 2, { status: 'dead', createAllocations: false, childrenCount: 0 });
   267        await JobsList.visit();
   268      },
   269      filter: (job, selection) => selection.includes(job.status),
   270    });
   271  
   272    testFacet('Datacenter', {
   273      facet: JobsList.facets.datacenter,
   274      paramName: 'dc',
   275      expectedOptions(jobs) {
   276        const allDatacenters = new Set(
   277          jobs.mapBy('datacenters').reduce((acc, val) => acc.concat(val), [])
   278        );
   279        return Array.from(allDatacenters).sort();
   280      },
   281      async beforeEach() {
   282        server.create('job', {
   283          datacenters: ['pdx', 'lax'],
   284          createAllocations: false,
   285          childrenCount: 0,
   286        });
   287        server.create('job', {
   288          datacenters: ['pdx', 'ord'],
   289          createAllocations: false,
   290          childrenCount: 0,
   291        });
   292        server.create('job', {
   293          datacenters: ['lax', 'jfk'],
   294          createAllocations: false,
   295          childrenCount: 0,
   296        });
   297        server.create('job', {
   298          datacenters: ['jfk', 'dfw'],
   299          createAllocations: false,
   300          childrenCount: 0,
   301        });
   302        server.create('job', { datacenters: ['pdx'], createAllocations: false, childrenCount: 0 });
   303        await JobsList.visit();
   304      },
   305      filter: (job, selection) => job.datacenters.find(dc => selection.includes(dc)),
   306    });
   307  
   308    testFacet('Prefix', {
   309      facet: JobsList.facets.prefix,
   310      paramName: 'prefix',
   311      expectedOptions: ['hashi (3)', 'nmd (2)', 'pre (2)'],
   312      async beforeEach() {
   313        [
   314          'pre-one',
   315          'hashi_one',
   316          'nmd.one',
   317          'one-alone',
   318          'pre_two',
   319          'hashi.two',
   320          'hashi-three',
   321          'nmd_two',
   322          'noprefix',
   323        ].forEach(name => {
   324          server.create('job', { name, createAllocations: false, childrenCount: 0 });
   325        });
   326        await JobsList.visit();
   327      },
   328      filter: (job, selection) => selection.find(prefix => job.name.startsWith(prefix)),
   329    });
   330  
   331    test('when the facet selections result in no matches, the empty state states why', async function(assert) {
   332      server.createList('job', 2, { status: 'pending', createAllocations: false, childrenCount: 0 });
   333  
   334      await JobsList.visit();
   335  
   336      await JobsList.facets.status.toggle();
   337      await JobsList.facets.status.options.objectAt(1).toggle();
   338      assert.ok(JobsList.isEmpty, 'There is an empty message');
   339      assert.equal(JobsList.emptyState.headline, 'No Matches', 'The message is appropriate');
   340    });
   341  
   342    test('the jobs list is immediately filtered based on query params', async function(assert) {
   343      server.create('job', { type: 'batch', createAllocations: false });
   344      server.create('job', { type: 'service', createAllocations: false });
   345  
   346      await JobsList.visit({ type: JSON.stringify(['batch']) });
   347  
   348      assert.equal(JobsList.jobs.length, 1, 'Only one job shown due to query param');
   349    });
   350  
   351    test('the active namespace is carried over to the storage pages', async function(assert) {
   352      server.createList('namespace', 2);
   353  
   354      const namespace = server.db.namespaces[1];
   355      await JobsList.visit({ namespace: namespace.id });
   356  
   357      await Layout.gutter.visitStorage();
   358  
   359      assert.equal(currentURL(), `/csi/volumes?namespace=${namespace.id}`);
   360    });
   361  
   362    pageSizeSelect({
   363      resourceName: 'job',
   364      pageObject: JobsList,
   365      pageObjectList: JobsList.jobs,
   366      async setup() {
   367        server.createList('job', JobsList.pageSize, { shallow: true, createAllocations: false });
   368        await JobsList.visit();
   369      },
   370    });
   371  
   372    function testFacet(label, { facet, paramName, beforeEach, filter, expectedOptions }) {
   373      test(`the ${label} facet has the correct options`, async function(assert) {
   374        await beforeEach();
   375        await facet.toggle();
   376  
   377        let expectation;
   378        if (typeof expectedOptions === 'function') {
   379          expectation = expectedOptions(server.db.jobs);
   380        } else {
   381          expectation = expectedOptions;
   382        }
   383  
   384        assert.deepEqual(
   385          facet.options.map(option => option.label.trim()),
   386          expectation,
   387          'Options for facet are as expected'
   388        );
   389      });
   390  
   391      test(`the ${label} facet filters the jobs list by ${label}`, async function(assert) {
   392        let option;
   393  
   394        await beforeEach();
   395        await facet.toggle();
   396  
   397        option = facet.options.objectAt(0);
   398        await option.toggle();
   399  
   400        const selection = [option.key];
   401        const expectedJobs = server.db.jobs
   402          .filter(job => filter(job, selection))
   403          .sortBy('modifyIndex')
   404          .reverse();
   405  
   406        JobsList.jobs.forEach((job, index) => {
   407          assert.equal(
   408            job.id,
   409            expectedJobs[index].id,
   410            `Job at ${index} is ${expectedJobs[index].id}`
   411          );
   412        });
   413      });
   414  
   415      test(`selecting multiple options in the ${label} facet results in a broader search`, async function(assert) {
   416        const selection = [];
   417  
   418        await beforeEach();
   419        await facet.toggle();
   420  
   421        const option1 = facet.options.objectAt(0);
   422        const option2 = facet.options.objectAt(1);
   423        await option1.toggle();
   424        selection.push(option1.key);
   425        await option2.toggle();
   426        selection.push(option2.key);
   427  
   428        const expectedJobs = server.db.jobs
   429          .filter(job => filter(job, selection))
   430          .sortBy('modifyIndex')
   431          .reverse();
   432  
   433        JobsList.jobs.forEach((job, index) => {
   434          assert.equal(
   435            job.id,
   436            expectedJobs[index].id,
   437            `Job at ${index} is ${expectedJobs[index].id}`
   438          );
   439        });
   440      });
   441  
   442      test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function(assert) {
   443        const selection = [];
   444  
   445        await beforeEach();
   446        await facet.toggle();
   447  
   448        const option1 = facet.options.objectAt(0);
   449        const option2 = facet.options.objectAt(1);
   450        await option1.toggle();
   451        selection.push(option1.key);
   452        await option2.toggle();
   453        selection.push(option2.key);
   454  
   455        assert.equal(
   456          currentURL(),
   457          `/jobs?${paramName}=${encodeURIComponent(JSON.stringify(selection))}`,
   458          'URL has the correct query param key and value'
   459        );
   460      });
   461  
   462      test('the run job button works when filters are set', async function(assert) {
   463        ['pre-one', 'pre-two', 'pre-three'].forEach(name => {
   464          server.create('job', { name, createAllocations: false, childrenCount: 0 });
   465        });
   466  
   467        await JobsList.visit();
   468  
   469        await JobsList.facets.prefix.toggle();
   470        await JobsList.facets.prefix.options[0].toggle();
   471  
   472        await JobsList.runJobButton.click();
   473        assert.equal(currentURL(), '/jobs/run');
   474      });
   475    }
   476  });