github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/topo-viz/node-test.js (about)

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