github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/integration/components/topo-viz/node-test.js (about)

     1  import { findAll, render } from '@ember/test-helpers';
     2  import { module, test } from 'qunit';
     3  import { setupRenderingTest } from 'ember-qunit';
     4  import hbs from 'htmlbars-inline-precompile';
     5  import { create } from 'ember-cli-page-object';
     6  import sinon from 'sinon';
     7  import faker from 'nomad-ui/mirage/faker';
     8  import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
     9  import { setupMirage } from 'ember-cli-mirage/test-support';
    10  import topoVisNodePageObject from 'nomad-ui/tests/pages/components/topo-viz/node';
    11  import {
    12    formatScheduledBytes,
    13    formatScheduledHertz,
    14  } from 'nomad-ui/utils/units';
    15  
    16  const TopoVizNode = create(topoVisNodePageObject());
    17  
    18  const nodeGen = (name, datacenter, memory, cpu, flags = {}) => ({
    19    datacenter,
    20    memory,
    21    cpu,
    22    isSelected: !!flags.isSelected,
    23    node: {
    24      name,
    25      isEligible: flags.isEligible || flags.isEligible == null,
    26      isDraining: !!flags.isDraining,
    27    },
    28  });
    29  
    30  const allocGen = (node, memory, cpu, isScheduled = true) => ({
    31    memory,
    32    cpu,
    33    isSelected: false,
    34    memoryPercent: memory / node.memory,
    35    cpuPercent: cpu / node.cpu,
    36    allocation: {
    37      id: faker.random.uuid(),
    38      isScheduled,
    39    },
    40  });
    41  
    42  const props = (overrides) => ({
    43    isDense: false,
    44    heightScale: () => 50,
    45    onAllocationSelect: sinon.spy(),
    46    onNodeSelect: sinon.spy(),
    47    onAllocationFocus: sinon.spy(),
    48    onAllocationBlur: sinon.spy(),
    49    ...overrides,
    50  });
    51  
    52  module('Integration | Component | TopoViz::Node', function (hooks) {
    53    setupRenderingTest(hooks);
    54    setupMirage(hooks);
    55  
    56    const commonTemplate = hbs`
    57      <TopoViz::Node
    58        @node={{this.node}}
    59        @isDense={{this.isDense}}
    60        @heightScale={{this.heightScale}}
    61        @onAllocationSelect={{this.onAllocationSelect}}
    62        @onAllocationFocus={{this.onAllocationFocus}}
    63        @onAllocationBlur={{this.onAllocationBlur}}
    64        @onNodeSelect={{this.onNodeSelect}} />
    65    `;
    66  
    67    test('presents as a div with a label and an svg with CPU and memory rows', async function (assert) {
    68      assert.expect(4);
    69  
    70      const node = nodeGen('Node One', 'dc1', 1000, 1000);
    71      this.setProperties(
    72        props({
    73          node: {
    74            ...node,
    75            allocations: [
    76              allocGen(node, 100, 100),
    77              allocGen(node, 250, 250),
    78              allocGen(node, 300, 300, false),
    79            ],
    80          },
    81        })
    82      );
    83  
    84      await render(commonTemplate);
    85  
    86      assert.ok(TopoVizNode.isPresent);
    87      assert.equal(
    88        TopoVizNode.memoryRects.length,
    89        this.node.allocations.filterBy('allocation.isScheduled').length
    90      );
    91      assert.ok(TopoVizNode.cpuRects.length);
    92  
    93      await componentA11yAudit(this.element, assert);
    94    });
    95  
    96    test('the label contains aggregate information about the node', async function (assert) {
    97      const node = nodeGen('Node One', 'dc1', 1000, 1000);
    98      this.setProperties(
    99        props({
   100          node: {
   101            ...node,
   102            allocations: [
   103              allocGen(node, 100, 100),
   104              allocGen(node, 250, 250),
   105              allocGen(node, 300, 300, false),
   106            ],
   107          },
   108        })
   109      );
   110  
   111      await render(commonTemplate);
   112  
   113      assert.ok(TopoVizNode.label.includes(node.node.name));
   114      assert.ok(
   115        TopoVizNode.label.includes(
   116          `${
   117            this.node.allocations.filterBy('allocation.isScheduled').length
   118          } Allocs`
   119        )
   120      );
   121      assert.ok(
   122        TopoVizNode.label.includes(
   123          `${formatScheduledBytes(this.node.memory, 'MiB')}`
   124        )
   125      );
   126      assert.ok(
   127        TopoVizNode.label.includes(
   128          `${formatScheduledHertz(this.node.cpu, 'MHz')}`
   129        )
   130      );
   131    });
   132  
   133    test('the status icon indicates when the node is draining', async function (assert) {
   134      const node = nodeGen('Node One', 'dc1', 1000, 1000, { isDraining: true });
   135      this.setProperties(
   136        props({
   137          node: {
   138            ...node,
   139            allocations: [],
   140          },
   141        })
   142      );
   143  
   144      await render(commonTemplate);
   145  
   146      assert.ok(TopoVizNode.statusIcon.includes('icon-is-clock-outline'));
   147      assert.equal(TopoVizNode.statusIconLabel, 'Client is draining');
   148    });
   149  
   150    test('the status icon indicates when the node is ineligible for scheduling', async function (assert) {
   151      const node = nodeGen('Node One', 'dc1', 1000, 1000, { isEligible: false });
   152      this.setProperties(
   153        props({
   154          node: {
   155            ...node,
   156            allocations: [],
   157          },
   158        })
   159      );
   160  
   161      await render(commonTemplate);
   162  
   163      assert.ok(TopoVizNode.statusIcon.includes('icon-is-lock-closed'));
   164      assert.equal(TopoVizNode.statusIconLabel, 'Client is ineligible');
   165    });
   166  
   167    test('when isDense is false, clicking the node does nothing', async function (assert) {
   168      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   169      this.setProperties(
   170        props({
   171          isDense: false,
   172          node: {
   173            ...node,
   174            allocations: [],
   175          },
   176        })
   177      );
   178  
   179      await render(commonTemplate);
   180      await TopoVizNode.selectNode();
   181  
   182      assert.notOk(TopoVizNode.nodeIsInteractive);
   183      assert.notOk(this.onNodeSelect.called);
   184    });
   185  
   186    test('when isDense is true, clicking the node calls onNodeSelect', async function (assert) {
   187      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   188      this.setProperties(
   189        props({
   190          isDense: true,
   191          node: {
   192            ...node,
   193            allocations: [],
   194          },
   195        })
   196      );
   197  
   198      await render(commonTemplate);
   199      await TopoVizNode.selectNode();
   200  
   201      assert.ok(TopoVizNode.nodeIsInteractive);
   202      assert.ok(this.onNodeSelect.called);
   203      assert.ok(this.onNodeSelect.calledWith(this.node));
   204    });
   205  
   206    test('the node gets the is-selected class when the node is selected', async function (assert) {
   207      const node = nodeGen('Node One', 'dc1', 1000, 1000, { isSelected: true });
   208      this.setProperties(
   209        props({
   210          isDense: true,
   211          node: {
   212            ...node,
   213            allocations: [],
   214          },
   215        })
   216      );
   217  
   218      await render(commonTemplate);
   219  
   220      assert.ok(TopoVizNode.nodeIsSelected);
   221    });
   222  
   223    test('the node gets its height form the @heightScale arg', async function (assert) {
   224      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   225      const height = 50;
   226      const heightSpy = sinon.spy();
   227      this.setProperties(
   228        props({
   229          heightScale: (...args) => {
   230            heightSpy(...args);
   231            return height;
   232          },
   233          node: {
   234            ...node,
   235            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   236          },
   237        })
   238      );
   239  
   240      await render(commonTemplate);
   241  
   242      assert.ok(heightSpy.called);
   243      assert.ok(heightSpy.calledWith(this.node.memory));
   244      assert.equal(TopoVizNode.memoryRects[0].height, `${height}px`);
   245    });
   246  
   247    test('each allocation gets a memory rect and a cpu rect', async function (assert) {
   248      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   249      this.setProperties(
   250        props({
   251          node: {
   252            ...node,
   253            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   254          },
   255        })
   256      );
   257  
   258      await render(commonTemplate);
   259  
   260      assert.equal(TopoVizNode.memoryRects.length, this.node.allocations.length);
   261      assert.equal(TopoVizNode.cpuRects.length, this.node.allocations.length);
   262    });
   263  
   264    test('each allocation is sized according to its percentage of utilization', async function (assert) {
   265      assert.expect(4);
   266  
   267      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   268      this.setProperties(
   269        props({
   270          node: {
   271            ...node,
   272            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   273          },
   274        })
   275      );
   276  
   277      await render(hbs`
   278        <div style="width:100px">
   279          <TopoViz::Node
   280            @node={{this.node}}
   281            @isDense={{this.isDense}}
   282            @heightScale={{this.heightScale}}
   283            @onAllocationSelect={{this.onAllocationSelect}}
   284            @onNodeSelect={{this.onNodeSelect}} />
   285        </div>
   286      `);
   287  
   288      // Remove the width of the padding and the label from the SVG width
   289      const width = 100 - 5 - 5 - 20;
   290      this.node.allocations.forEach((alloc, index) => {
   291        const memWidth = alloc.memoryPercent * width - (index === 0 ? 0.5 : 1);
   292        const cpuWidth = alloc.cpuPercent * width - (index === 0 ? 0.5 : 1);
   293        assert.equal(TopoVizNode.memoryRects[index].width, `${memWidth}px`);
   294        assert.equal(TopoVizNode.cpuRects[index].width, `${cpuWidth}px`);
   295      });
   296    });
   297  
   298    test('clicking either the memory or cpu rect for an allocation will call onAllocationSelect', async function (assert) {
   299      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   300      this.setProperties(
   301        props({
   302          node: {
   303            ...node,
   304            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   305          },
   306        })
   307      );
   308  
   309      await render(commonTemplate);
   310  
   311      await TopoVizNode.memoryRects[0].select();
   312      assert.equal(this.onAllocationSelect.callCount, 1);
   313      assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[0]));
   314  
   315      await TopoVizNode.cpuRects[0].select();
   316      assert.equal(this.onAllocationSelect.callCount, 2);
   317  
   318      await TopoVizNode.cpuRects[1].select();
   319      assert.equal(this.onAllocationSelect.callCount, 3);
   320      assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[1]));
   321  
   322      await TopoVizNode.memoryRects[1].select();
   323      assert.equal(this.onAllocationSelect.callCount, 4);
   324    });
   325  
   326    test('hovering over a memory or cpu rect for an allocation will call onAllocationFocus', async function (assert) {
   327      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   328      this.setProperties(
   329        props({
   330          node: {
   331            ...node,
   332            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   333          },
   334        })
   335      );
   336  
   337      await render(commonTemplate);
   338  
   339      await TopoVizNode.memoryRects[0].hover();
   340      assert.equal(this.onAllocationFocus.callCount, 1);
   341      assert.equal(
   342        this.onAllocationFocus.getCall(0).args[0].allocation,
   343        this.node.allocations[0].allocation
   344      );
   345      assert.equal(
   346        this.onAllocationFocus.getCall(0).args[1],
   347        findAll('[data-test-memory-rect]')[0]
   348      );
   349  
   350      await TopoVizNode.cpuRects[1].hover();
   351      assert.equal(this.onAllocationFocus.callCount, 2);
   352      assert.equal(
   353        this.onAllocationFocus.getCall(1).args[0].allocation,
   354        this.node.allocations[1].allocation
   355      );
   356      assert.equal(
   357        this.onAllocationFocus.getCall(1).args[1],
   358        findAll('[data-test-cpu-rect]')[1]
   359      );
   360    });
   361  
   362    test('leaving the entire node will call onAllocationBlur, which allows for the tooltip transitions', async function (assert) {
   363      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   364      this.setProperties(
   365        props({
   366          node: {
   367            ...node,
   368            allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
   369          },
   370        })
   371      );
   372  
   373      await render(commonTemplate);
   374  
   375      await TopoVizNode.memoryRects[0].hover();
   376      assert.equal(this.onAllocationFocus.callCount, 1);
   377      assert.equal(this.onAllocationBlur.callCount, 0);
   378  
   379      await TopoVizNode.memoryRects[0].mouseleave();
   380      assert.equal(this.onAllocationBlur.callCount, 0);
   381  
   382      await TopoVizNode.mouseout();
   383      assert.equal(this.onAllocationBlur.callCount, 1);
   384    });
   385  
   386    test('allocations are sorted by smallest to largest delta of memory to cpu percent utilizations', async function (assert) {
   387      assert.expect(10);
   388  
   389      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   390  
   391      const evenAlloc = allocGen(node, 100, 100);
   392      const mediumMemoryAlloc = allocGen(node, 200, 150);
   393      const largeMemoryAlloc = allocGen(node, 300, 50);
   394      const mediumCPUAlloc = allocGen(node, 150, 200);
   395      const largeCPUAlloc = allocGen(node, 50, 300);
   396  
   397      this.setProperties(
   398        props({
   399          node: {
   400            ...node,
   401            allocations: [
   402              largeCPUAlloc,
   403              mediumCPUAlloc,
   404              evenAlloc,
   405              mediumMemoryAlloc,
   406              largeMemoryAlloc,
   407            ],
   408          },
   409        })
   410      );
   411  
   412      await render(commonTemplate);
   413  
   414      const expectedOrder = [
   415        evenAlloc,
   416        mediumCPUAlloc,
   417        mediumMemoryAlloc,
   418        largeCPUAlloc,
   419        largeMemoryAlloc,
   420      ];
   421      expectedOrder.forEach((alloc, index) => {
   422        assert.equal(TopoVizNode.memoryRects[index].id, alloc.allocation.id);
   423        assert.equal(TopoVizNode.cpuRects[index].id, alloc.allocation.id);
   424      });
   425    });
   426  
   427    test('when there are no allocations, a "no allocations" note is shown', async function (assert) {
   428      const node = nodeGen('Node One', 'dc1', 1000, 1000);
   429      this.setProperties(
   430        props({
   431          node: {
   432            ...node,
   433            allocations: [],
   434          },
   435        })
   436      );
   437  
   438      await render(commonTemplate);
   439      assert.equal(TopoVizNode.emptyMessage, 'Empty Client');
   440    });
   441  });