github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/clients-list-test.js (about)

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