github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/clients-list-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  /* eslint-disable qunit/require-expect */
     7  import { currentURL, settled } from '@ember/test-helpers';
     8  import { module, test } from 'qunit';
     9  import { setupApplicationTest } from 'ember-qunit';
    10  import { setupMirage } from 'ember-cli-mirage/test-support';
    11  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
    12  import pageSizeSelect from './behaviors/page-size-select';
    13  import ClientsList from 'nomad-ui/tests/pages/clients/list';
    14  import percySnapshot from '@percy/ember';
    15  import faker from 'nomad-ui/mirage/faker';
    16  
    17  module('Acceptance | clients list', function (hooks) {
    18    setupApplicationTest(hooks);
    19    setupMirage(hooks);
    20  
    21    hooks.beforeEach(function () {
    22      window.localStorage.clear();
    23      server.createList('node-pool', 3);
    24    });
    25  
    26    test('it passes an accessibility audit', async function (assert) {
    27      const nodesCount = ClientsList.pageSize + 1;
    28  
    29      server.createList('node', nodesCount);
    30      server.createList('agent', 1);
    31  
    32      await ClientsList.visit();
    33      await a11yAudit(assert);
    34    });
    35  
    36    test('/clients should list one page of clients', async function (assert) {
    37      faker.seed(1);
    38      // Make sure to make more nodes than 1 page to assert that pagination is working
    39      const nodesCount = ClientsList.pageSize + 1;
    40      server.createList('node', nodesCount);
    41      server.createList('agent', 1);
    42  
    43      await ClientsList.visit();
    44  
    45      await percySnapshot(assert);
    46  
    47      assert.equal(ClientsList.nodes.length, ClientsList.pageSize);
    48      assert.ok(ClientsList.hasPagination, 'Pagination found on the page');
    49  
    50      const sortedNodes = server.db.nodes.sortBy('modifyIndex').reverse();
    51  
    52      ClientsList.nodes.forEach((node, index) => {
    53        assert.equal(
    54          node.id,
    55          sortedNodes[index].id.split('-')[0],
    56          'Clients are ordered'
    57        );
    58      });
    59  
    60      assert.ok(document.title.includes('Clients'));
    61    });
    62  
    63    test('each client record should show high-level info of the client', async function (assert) {
    64      const node = server.create('node', 'draining', {
    65        status: 'ready',
    66      });
    67  
    68      server.createList('agent', 1);
    69  
    70      await ClientsList.visit();
    71  
    72      const nodeRow = ClientsList.nodes.objectAt(0);
    73      const allocations = server.db.allocations.where({ nodeId: node.id });
    74  
    75      assert.equal(nodeRow.id, node.id.split('-')[0], 'ID');
    76      assert.equal(nodeRow.name, node.name, 'Name');
    77      assert.equal(nodeRow.nodePool, node.nodePool, 'Node Pool');
    78      assert.equal(
    79        nodeRow.compositeStatus.text,
    80        'draining',
    81        'Combined status, draining, and eligbility'
    82      );
    83      assert.equal(nodeRow.address, node.httpAddr);
    84      assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter');
    85      assert.equal(nodeRow.version, node.version, 'Version');
    86      assert.equal(nodeRow.allocations, allocations.length, '# Allocations');
    87    });
    88  
    89    test('each client record should show running allocations', async function (assert) {
    90      server.createList('agent', 1);
    91  
    92      const node = server.create('node', {
    93        modifyIndex: 4,
    94        status: 'ready',
    95        schedulingEligibility: 'eligible',
    96        drain: false,
    97      });
    98  
    99      server.create('job', { createAllocations: false });
   100  
   101      const running = server.createList('allocation', 2, {
   102        clientStatus: 'running',
   103      });
   104      server.createList('allocation', 3, { clientStatus: 'pending' });
   105      server.createList('allocation', 10, { clientStatus: 'complete' });
   106  
   107      await ClientsList.visit();
   108  
   109      const nodeRow = ClientsList.nodes.objectAt(0);
   110  
   111      assert.equal(nodeRow.id, node.id.split('-')[0], 'ID');
   112      assert.equal(
   113        nodeRow.compositeStatus.text,
   114        'ready',
   115        'Combined status, draining, and eligbility'
   116      );
   117      assert.equal(nodeRow.allocations, running.length, '# Allocations');
   118    });
   119  
   120    test('client status, draining, and eligibility are collapsed into one column that stays sorted', async function (assert) {
   121      server.createList('agent', 1);
   122  
   123      server.create('node', {
   124        modifyIndex: 5,
   125        status: 'ready',
   126        schedulingEligibility: 'eligible',
   127        drain: false,
   128      });
   129      server.create('node', {
   130        modifyIndex: 4,
   131        status: 'initializing',
   132        schedulingEligibility: 'eligible',
   133        drain: false,
   134      });
   135      server.create('node', {
   136        modifyIndex: 3,
   137        status: 'down',
   138        schedulingEligibility: 'eligible',
   139        drain: false,
   140      });
   141      server.create('node', {
   142        modifyIndex: 2,
   143        status: 'down',
   144        schedulingEligibility: 'ineligible',
   145        drain: false,
   146      });
   147      server.create('node', {
   148        modifyIndex: 1,
   149        status: 'ready',
   150        schedulingEligibility: 'ineligible',
   151        drain: false,
   152      });
   153      server.create('node', 'draining', {
   154        modifyIndex: 0,
   155        status: 'ready',
   156      });
   157  
   158      await ClientsList.visit();
   159  
   160      ClientsList.nodes[0].compositeStatus.as((readyClient) => {
   161        assert.equal(readyClient.text, 'ready');
   162        assert.ok(readyClient.isUnformatted, 'expected no status class');
   163        assert.equal(readyClient.tooltip, 'ready / not draining / eligible');
   164      });
   165  
   166      assert.equal(ClientsList.nodes[1].compositeStatus.text, 'initializing');
   167      assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down');
   168      assert.equal(
   169        ClientsList.nodes[2].compositeStatus.text,
   170        'down',
   171        'down takes priority over ineligible'
   172      );
   173  
   174      assert.equal(ClientsList.nodes[4].compositeStatus.text, 'ineligible');
   175      assert.ok(
   176        ClientsList.nodes[4].compositeStatus.isWarning,
   177        'expected warning class'
   178      );
   179  
   180      assert.equal(ClientsList.nodes[5].compositeStatus.text, 'draining');
   181      assert.ok(
   182        ClientsList.nodes[5].compositeStatus.isInfo,
   183        'expected info class'
   184      );
   185  
   186      await ClientsList.sortBy('compositeStatus');
   187  
   188      assert.deepEqual(
   189        ClientsList.nodes.map((n) => n.compositeStatus.text),
   190        ['ready', 'initializing', 'ineligible', 'draining', 'down', 'down']
   191      );
   192  
   193      // Simulate a client state change arriving through polling
   194      let readyClient = this.owner
   195        .lookup('service:store')
   196        .peekAll('node')
   197        .findBy('modifyIndex', 5);
   198      readyClient.set('schedulingEligibility', 'ineligible');
   199  
   200      await settled();
   201  
   202      assert.deepEqual(
   203        ClientsList.nodes.map((n) => n.compositeStatus.text),
   204        ['initializing', 'ineligible', 'ineligible', 'draining', 'down', 'down']
   205      );
   206    });
   207  
   208    test('each client should link to the client detail page', async function (assert) {
   209      server.createList('node', 1);
   210      server.createList('agent', 1);
   211  
   212      const node = server.db.nodes[0];
   213  
   214      await ClientsList.visit();
   215      await ClientsList.nodes.objectAt(0).clickRow();
   216  
   217      assert.equal(currentURL(), `/clients/${node.id}`);
   218    });
   219  
   220    test('when there are no clients, there is an empty message', async function (assert) {
   221      faker.seed(1);
   222      server.createList('agent', 1);
   223  
   224      await ClientsList.visit();
   225  
   226      await percySnapshot(assert);
   227  
   228      assert.ok(ClientsList.isEmpty);
   229      assert.equal(ClientsList.empty.headline, 'No Clients');
   230    });
   231  
   232    test('when there are clients, but no matches for a search term, there is an empty message', async function (assert) {
   233      server.createList('agent', 1);
   234      server.create('node', { name: 'node' });
   235  
   236      await ClientsList.visit();
   237  
   238      await ClientsList.search('client');
   239      assert.ok(ClientsList.isEmpty);
   240      assert.equal(ClientsList.empty.headline, 'No Matches');
   241    });
   242  
   243    test('when accessing clients is forbidden, show a message with a link to the tokens page', async function (assert) {
   244      server.create('agent');
   245      server.create('node', { name: 'node' });
   246      server.pretender.get('/v1/nodes', () => [403, {}, null]);
   247  
   248      await ClientsList.visit();
   249  
   250      assert.equal(ClientsList.error.title, 'Not Authorized');
   251  
   252      await ClientsList.error.seekHelp();
   253  
   254      assert.equal(currentURL(), '/settings/tokens');
   255    });
   256  
   257    pageSizeSelect({
   258      resourceName: 'client',
   259      pageObject: ClientsList,
   260      pageObjectList: ClientsList.nodes,
   261      async setup() {
   262        server.createList('node', ClientsList.pageSize);
   263        server.createList('agent', 1);
   264        await ClientsList.visit();
   265      },
   266    });
   267  
   268    testFacet('Class', {
   269      facet: ClientsList.facets.class,
   270      paramName: 'class',
   271      expectedOptions(nodes) {
   272        return Array.from(new Set(nodes.mapBy('nodeClass'))).sort();
   273      },
   274      async beforeEach() {
   275        server.create('agent');
   276        server.createList('node', 2, { nodeClass: 'nc-one' });
   277        server.createList('node', 2, { nodeClass: 'nc-two' });
   278        server.createList('node', 2, { nodeClass: 'nc-three' });
   279        await ClientsList.visit();
   280      },
   281      filter: (node, selection) => selection.includes(node.nodeClass),
   282    });
   283  
   284    testFacet('State', {
   285      facet: ClientsList.facets.state,
   286      paramName: 'state',
   287      expectedOptions: [
   288        'Initializing',
   289        'Ready',
   290        'Down',
   291        'Ineligible',
   292        'Draining',
   293        'Disconnected',
   294      ],
   295      async beforeEach() {
   296        server.create('agent');
   297  
   298        server.createList('node', 2, { status: 'initializing' });
   299        server.createList('node', 2, { status: 'ready' });
   300        server.createList('node', 2, { status: 'down' });
   301  
   302        server.createList('node', 2, {
   303          schedulingEligibility: 'eligible',
   304          drain: false,
   305        });
   306        server.createList('node', 2, {
   307          schedulingEligibility: 'ineligible',
   308          drain: false,
   309        });
   310        server.createList('node', 2, {
   311          schedulingEligibility: 'ineligible',
   312          drain: true,
   313        });
   314  
   315        await ClientsList.visit();
   316      },
   317      filter: (node, selection) => {
   318        if (selection.includes('draining') && !node.drain) return false;
   319        if (
   320          selection.includes('ineligible') &&
   321          node.schedulingEligibility === 'eligible'
   322        )
   323          return false;
   324  
   325        return selection.includes(node.status);
   326      },
   327    });
   328  
   329    testFacet('Node Pools', {
   330      facet: ClientsList.facets.nodePools,
   331      paramName: 'nodePool',
   332      expectedOptions() {
   333        return server.db.nodePools
   334          .filter((p) => p.name !== 'all') // The node pool 'all' should not be a filter.
   335          .map((p) => p.name);
   336      },
   337      async beforeEach() {
   338        server.create('agent');
   339        server.create('node-pool', { name: 'all' });
   340        server.create('node-pool', { name: 'default' });
   341        server.createList('node-pool', 10);
   342  
   343        // Make sure each node pool has at least one node.
   344        server.db.nodePools.forEach((p) => {
   345          server.createList('node', 2, { nodePool: p.name });
   346        });
   347        await ClientsList.visit();
   348      },
   349      filter: (node, selection) => selection.includes(node.nodePool),
   350    });
   351  
   352    testFacet('Datacenters', {
   353      facet: ClientsList.facets.datacenter,
   354      paramName: 'dc',
   355      expectedOptions(nodes) {
   356        return Array.from(new Set(nodes.mapBy('datacenter'))).sort();
   357      },
   358      async beforeEach() {
   359        server.create('agent');
   360        server.createList('node', 2, { datacenter: 'pdx-1' });
   361        server.createList('node', 2, { datacenter: 'nyc-1' });
   362        server.createList('node', 2, { datacenter: 'ams-1' });
   363        await ClientsList.visit();
   364      },
   365      filter: (node, selection) => selection.includes(node.datacenter),
   366    });
   367  
   368    testFacet('Versions', {
   369      facet: ClientsList.facets.version,
   370      paramName: 'version',
   371      expectedOptions(nodes) {
   372        return Array.from(new Set(nodes.mapBy('version'))).sort();
   373      },
   374      async beforeEach() {
   375        server.create('agent');
   376        server.createList('node', 2, { version: '0.12.0' });
   377        server.createList('node', 2, { version: '1.1.0-beta1' });
   378        server.createList('node', 2, { version: '1.2.0+ent' });
   379        await ClientsList.visit();
   380      },
   381      filter: (node, selection) => selection.includes(node.version),
   382    });
   383  
   384    testFacet('Volumes', {
   385      facet: ClientsList.facets.volume,
   386      paramName: 'volume',
   387      expectedOptions(nodes) {
   388        const flatten = (acc, val) => acc.concat(Object.keys(val));
   389        return Array.from(
   390          new Set(nodes.mapBy('hostVolumes').reduce(flatten, []))
   391        );
   392      },
   393      async beforeEach() {
   394        server.create('agent');
   395        server.createList('node', 2, { hostVolumes: { One: { Name: 'One' } } });
   396        server.createList('node', 2, {
   397          hostVolumes: { One: { Name: 'One' }, Two: { Name: 'Two' } },
   398        });
   399        server.createList('node', 2, { hostVolumes: { Two: { Name: 'Two' } } });
   400        await ClientsList.visit();
   401      },
   402      filter: (node, selection) =>
   403        Object.keys(node.hostVolumes).find((volume) =>
   404          selection.includes(volume)
   405        ),
   406    });
   407  
   408    test('when the facet selections result in no matches, the empty state states why', async function (assert) {
   409      server.create('agent');
   410      server.createList('node', 2, { status: 'ready' });
   411  
   412      await ClientsList.visit();
   413  
   414      await ClientsList.facets.state.toggle();
   415      await ClientsList.facets.state.options.objectAt(0).toggle();
   416      assert.ok(ClientsList.isEmpty, 'There is an empty message');
   417      assert.equal(
   418        ClientsList.empty.headline,
   419        'No Matches',
   420        'The message is appropriate'
   421      );
   422    });
   423  
   424    test('the clients list is immediately filtered based on query params', async function (assert) {
   425      server.create('agent');
   426      server.create('node', { nodeClass: 'omg-large' });
   427      server.create('node', { nodeClass: 'wtf-tiny' });
   428  
   429      await ClientsList.visit({ class: JSON.stringify(['wtf-tiny']) });
   430  
   431      assert.equal(
   432        ClientsList.nodes.length,
   433        1,
   434        'Only one client shown due to query param'
   435      );
   436    });
   437  
   438    function testFacet(
   439      label,
   440      { facet, paramName, beforeEach, filter, expectedOptions }
   441    ) {
   442      test(`the ${label} facet has the correct options`, async function (assert) {
   443        await beforeEach();
   444        await facet.toggle();
   445  
   446        let expectation;
   447        if (typeof expectedOptions === 'function') {
   448          expectation = expectedOptions(server.db.nodes);
   449        } else {
   450          expectation = expectedOptions;
   451        }
   452  
   453        assert.deepEqual(
   454          facet.options.map((option) => option.label.trim()),
   455          expectation,
   456          'Options for facet are as expected'
   457        );
   458      });
   459  
   460      test(`the ${label} facet filters the nodes list by ${label}`, async function (assert) {
   461        let option;
   462  
   463        await beforeEach();
   464  
   465        await facet.toggle();
   466        option = facet.options.objectAt(0);
   467        await option.toggle();
   468  
   469        const selection = [option.key];
   470        const expectedNodes = server.db.nodes
   471          .filter((node) => filter(node, selection))
   472          .sortBy('modifyIndex')
   473          .reverse();
   474  
   475        ClientsList.nodes.forEach((node, index) => {
   476          assert.equal(
   477            node.id,
   478            expectedNodes[index].id.split('-')[0],
   479            `Node at ${index} is ${expectedNodes[index].id}`
   480          );
   481        });
   482      });
   483  
   484      test(`selecting multiple options in the ${label} facet results in a broader search`, async function (assert) {
   485        const selection = [];
   486  
   487        await beforeEach();
   488        await facet.toggle();
   489  
   490        const option1 = facet.options.objectAt(0);
   491        const option2 = facet.options.objectAt(1);
   492        await option1.toggle();
   493        selection.push(option1.key);
   494        await option2.toggle();
   495        selection.push(option2.key);
   496  
   497        const expectedNodes = server.db.nodes
   498          .filter((node) => filter(node, selection))
   499          .sortBy('modifyIndex')
   500          .reverse();
   501  
   502        ClientsList.nodes.forEach((node, index) => {
   503          assert.equal(
   504            node.id,
   505            expectedNodes[index].id.split('-')[0],
   506            `Node at ${index} is ${expectedNodes[index].id}`
   507          );
   508        });
   509      });
   510  
   511      test(`selecting options in the ${label} facet updates the ${paramName} query param`, async function (assert) {
   512        const selection = [];
   513  
   514        await beforeEach();
   515        await facet.toggle();
   516  
   517        const option1 = facet.options.objectAt(0);
   518        const option2 = facet.options.objectAt(1);
   519        await option1.toggle();
   520        selection.push(option1.key);
   521        await option2.toggle();
   522        selection.push(option2.key);
   523  
   524        assert.equal(
   525          currentURL(),
   526          `/clients?${paramName}=${encodeURIComponent(
   527            JSON.stringify(selection)
   528          )}`,
   529          'URL has the correct query param key and value'
   530        );
   531      });
   532    }
   533  });