github.com/hernad/nomad@v1.6.112/ui/tests/integration/components/das/recommendation-card-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import { module, test } from 'qunit';
     7  import { setupRenderingTest } from 'ember-qunit';
     8  import { render, settled } from '@ember/test-helpers';
     9  import { hbs } from 'ember-cli-htmlbars';
    10  import Service from '@ember/service';
    11  import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
    12  
    13  import RecommendationCardComponent from 'nomad-ui/tests/pages/components/recommendation-card';
    14  import { create } from 'ember-cli-page-object';
    15  const RecommendationCard = create(RecommendationCardComponent);
    16  
    17  import { tracked } from '@glimmer/tracking';
    18  import { action } from '@ember/object';
    19  import { set } from '@ember/object';
    20  
    21  module('Integration | Component | das/recommendation-card', function (hooks) {
    22    setupRenderingTest(hooks);
    23  
    24    hooks.beforeEach(function () {
    25      const mockRouter = Service.extend({
    26        init() {
    27          this._super(...arguments);
    28        },
    29  
    30        urlFor(route, slug, { queryParams: { namespace } }) {
    31          return `${route}:${slug}?namespace=${namespace}`;
    32        },
    33      });
    34  
    35      this.owner.register('service:router', mockRouter);
    36    });
    37  
    38    test('it renders a recommendation card', async function (assert) {
    39      assert.expect(49);
    40  
    41      const task1 = {
    42        name: 'jortle',
    43        reservedCPU: 150,
    44        reservedMemory: 128,
    45      };
    46  
    47      const task2 = {
    48        name: 'tortle',
    49        reservedCPU: 125,
    50        reservedMemory: 256,
    51      };
    52  
    53      this.set(
    54        'summary',
    55        new MockRecommendationSummary({
    56          jobNamespace: 'namespace',
    57          recommendations: [
    58            {
    59              resource: 'MemoryMB',
    60              stats: {},
    61              task: task1,
    62              value: 192,
    63              currentValue: task1.reservedMemory,
    64            },
    65            {
    66              resource: 'CPU',
    67              stats: {},
    68              task: task1,
    69              value: 50,
    70              currentValue: task1.reservedCPU,
    71            },
    72            {
    73              resource: 'CPU',
    74              stats: {},
    75              task: task2,
    76              value: 150,
    77              currentValue: task2.reservedCPU,
    78            },
    79            {
    80              resource: 'MemoryMB',
    81              stats: {},
    82              task: task2,
    83              value: 320,
    84              currentValue: task2.reservedMemory,
    85            },
    86          ],
    87  
    88          taskGroup: {
    89            count: 2,
    90            name: 'group-name',
    91            job: {
    92              name: 'job-name',
    93              namespace: {
    94                name: 'namespace',
    95              },
    96            },
    97            reservedCPU: task1.reservedCPU + task2.reservedCPU,
    98            reservedMemory: task1.reservedMemory + task2.reservedMemory,
    99          },
   100        })
   101      );
   102  
   103      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   104  
   105      assert.equal(RecommendationCard.slug.jobName, 'job-name');
   106      assert.equal(RecommendationCard.slug.groupName, 'group-name');
   107  
   108      assert.equal(RecommendationCard.namespace, 'namespace');
   109  
   110      assert.equal(RecommendationCard.totalsTable.current.cpu.text, '275 MHz');
   111      assert.equal(RecommendationCard.totalsTable.current.memory.text, '384 MiB');
   112  
   113      RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => {
   114        assert.equal(RecommendedCpu.text, '200 MHz');
   115        assert.ok(RecommendedCpu.isDecrease);
   116      });
   117  
   118      RecommendationCard.totalsTable.recommended.memory.as(
   119        (RecommendedMemory) => {
   120          assert.equal(RecommendedMemory.text, '512 MiB');
   121          assert.ok(RecommendedMemory.isIncrease);
   122        }
   123      );
   124  
   125      assert.equal(RecommendationCard.totalsTable.unitDiff.cpu, '-75 MHz');
   126      assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '+128 MiB');
   127  
   128      // Expected signal has a minus character, not a hyphen.
   129      assert.equal(RecommendationCard.totalsTable.percentDiff.cpu, '−27%');
   130      assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+33%');
   131  
   132      assert.dom('.copy-button').hasTextContaining('job-name / group-name');
   133  
   134      const clipboardText = document
   135        .querySelector('.copy-button > button')
   136        .getAttribute('data-clipboard-text');
   137      assert.ok(
   138        clipboardText.endsWith(
   139          'optimize.summary:job-name/group-name?namespace=namespace'
   140        )
   141      );
   142  
   143      assert.equal(
   144        RecommendationCard.activeTask.totalsTable.current.cpu.text,
   145        '150 MHz'
   146      );
   147      assert.equal(
   148        RecommendationCard.activeTask.totalsTable.current.memory.text,
   149        '128 MiB'
   150      );
   151  
   152      RecommendationCard.activeTask.totalsTable.recommended.cpu.as(
   153        (RecommendedCpu) => {
   154          assert.equal(RecommendedCpu.text, '50 MHz');
   155          assert.ok(RecommendedCpu.isDecrease);
   156        }
   157      );
   158  
   159      RecommendationCard.activeTask.totalsTable.recommended.memory.as(
   160        (RecommendedMemory) => {
   161          assert.equal(RecommendedMemory.text, '192 MiB');
   162          assert.ok(RecommendedMemory.isIncrease);
   163        }
   164      );
   165  
   166      assert.equal(RecommendationCard.activeTask.charts.length, 2);
   167      assert.equal(
   168        RecommendationCard.activeTask.charts[0].resource,
   169        'CPU',
   170        'CPU chart should be first when present'
   171      );
   172  
   173      assert.ok(RecommendationCard.activeTask.cpuChart.isDecrease);
   174      assert.ok(RecommendationCard.activeTask.memoryChart.isIncrease);
   175  
   176      assert.equal(RecommendationCard.togglesTable.tasks.length, 2);
   177  
   178      await RecommendationCard.togglesTable.tasks[0].as(async (FirstTask) => {
   179        assert.equal(FirstTask.name, 'jortle');
   180        assert.ok(FirstTask.isActive);
   181  
   182        assert.equal(FirstTask.cpu.title, 'CPU for jortle');
   183        assert.ok(FirstTask.cpu.isActive);
   184  
   185        assert.equal(FirstTask.memory.title, 'Memory for jortle');
   186        assert.ok(FirstTask.memory.isActive);
   187  
   188        await FirstTask.cpu.toggle();
   189  
   190        assert.notOk(FirstTask.cpu.isActive);
   191        assert.ok(RecommendationCard.activeTask.cpuChart.isDisabled);
   192      });
   193  
   194      assert.notOk(RecommendationCard.togglesTable.tasks[1].isActive);
   195  
   196      assert.equal(RecommendationCard.activeTask.name, 'jortle task');
   197  
   198      RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => {
   199        assert.equal(RecommendedCpu.text, '300 MHz');
   200        assert.ok(RecommendedCpu.isIncrease);
   201      });
   202  
   203      RecommendationCard.activeTask.totalsTable.recommended.cpu.as(
   204        (RecommendedCpu) => {
   205          assert.equal(RecommendedCpu.text, '150 MHz');
   206          assert.ok(RecommendedCpu.isNeutral);
   207        }
   208      );
   209  
   210      await RecommendationCard.togglesTable.toggleAllMemory.toggle();
   211  
   212      assert.notOk(RecommendationCard.togglesTable.tasks[0].memory.isActive);
   213      assert.notOk(RecommendationCard.togglesTable.tasks[1].memory.isActive);
   214  
   215      RecommendationCard.totalsTable.recommended.memory.as(
   216        (RecommendedMemory) => {
   217          assert.equal(RecommendedMemory.text, '384 MiB');
   218          assert.ok(RecommendedMemory.isNeutral);
   219        }
   220      );
   221  
   222      await RecommendationCard.togglesTable.tasks[1].click();
   223  
   224      assert.notOk(RecommendationCard.togglesTable.tasks[0].isActive);
   225      assert.ok(RecommendationCard.togglesTable.tasks[1].isActive);
   226  
   227      assert.equal(RecommendationCard.activeTask.name, 'tortle task');
   228      assert.equal(
   229        RecommendationCard.activeTask.totalsTable.current.cpu.text,
   230        '125 MHz'
   231      );
   232  
   233      await componentA11yAudit(this.element, assert);
   234    });
   235  
   236    test('it doesn’t have header toggles when there’s only one task', async function (assert) {
   237      const task1 = {
   238        name: 'jortle',
   239        reservedCPU: 150,
   240        reservedMemory: 128,
   241      };
   242  
   243      this.set(
   244        'summary',
   245        new MockRecommendationSummary({
   246          recommendations: [
   247            {
   248              resource: 'CPU',
   249              stats: {},
   250              task: task1,
   251              value: 50,
   252            },
   253            {
   254              resource: 'MemoryMB',
   255              stats: {},
   256              task: task1,
   257              value: 192,
   258            },
   259          ],
   260  
   261          taskGroup: {
   262            count: 1,
   263            reservedCPU: task1.reservedCPU,
   264            reservedMemory: task1.reservedMemory,
   265          },
   266        })
   267      );
   268  
   269      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   270  
   271      assert.notOk(RecommendationCard.togglesTable.toggleAllIsPresent);
   272      assert.notOk(RecommendationCard.togglesTable.toggleAllCPU.isPresent);
   273      assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isPresent);
   274    });
   275  
   276    test('it disables the accept button when all recommendations are disabled', async function (assert) {
   277      const task1 = {
   278        name: 'jortle',
   279        reservedCPU: 150,
   280        reservedMemory: 128,
   281      };
   282  
   283      this.set(
   284        'summary',
   285        new MockRecommendationSummary({
   286          recommendations: [
   287            {
   288              resource: 'CPU',
   289              stats: {},
   290              task: task1,
   291              value: 50,
   292            },
   293            {
   294              resource: 'MemoryMB',
   295              stats: {},
   296              task: task1,
   297              value: 192,
   298            },
   299          ],
   300  
   301          taskGroup: {
   302            count: 1,
   303            reservedCPU: task1.reservedCPU,
   304            reservedMemory: task1.reservedMemory,
   305          },
   306        })
   307      );
   308  
   309      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   310  
   311      await RecommendationCard.togglesTable.tasks[0].cpu.toggle();
   312      await RecommendationCard.togglesTable.tasks[0].memory.toggle();
   313  
   314      assert.ok(RecommendationCard.acceptButton.isDisabled);
   315    });
   316  
   317    test('it doesn’t show a toggle or chart when there’s no recommendation for that resource', async function (assert) {
   318      const task1 = {
   319        name: 'jortle',
   320        reservedCPU: 150,
   321        reservedMemory: 128,
   322      };
   323  
   324      this.set(
   325        'summary',
   326        new MockRecommendationSummary({
   327          recommendations: [
   328            {
   329              resource: 'CPU',
   330              stats: {},
   331              task: task1,
   332              value: 50,
   333            },
   334          ],
   335  
   336          taskGroup: {
   337            count: 2,
   338            name: 'group-name',
   339            job: {
   340              name: 'job-name',
   341            },
   342            reservedCPU: task1.reservedCPU,
   343            reservedMemory: task1.reservedMemory,
   344          },
   345        })
   346      );
   347  
   348      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   349  
   350      assert.equal(
   351        RecommendationCard.totalsTable.recommended.memory.text,
   352        '128 MiB'
   353      );
   354      assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '0 MiB');
   355      assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+0%');
   356  
   357      assert.equal(
   358        RecommendationCard.narrative.trim(),
   359        'Applying the selected recommendations will save an aggregate 200 MHz of CPU across 2 allocations.'
   360      );
   361  
   362      assert.ok(RecommendationCard.togglesTable.tasks[0].memory.isDisabled);
   363      assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent);
   364    });
   365  
   366    test('it disables a resource’s toggle all toggle when there are no recommendations for it', async function (assert) {
   367      const task1 = {
   368        name: 'jortle',
   369        reservedCPU: 150,
   370        reservedMemory: 128,
   371      };
   372  
   373      const task2 = {
   374        name: 'tortle',
   375        reservedCPU: 150,
   376        reservedMemory: 128,
   377      };
   378  
   379      this.set(
   380        'summary',
   381        new MockRecommendationSummary({
   382          recommendations: [
   383            {
   384              resource: 'CPU',
   385              stats: {},
   386              task: task1,
   387              value: 50,
   388            },
   389            {
   390              resource: 'CPU',
   391              stats: {},
   392              task: task2,
   393              value: 50,
   394            },
   395          ],
   396  
   397          taskGroup: {
   398            count: 2,
   399            name: 'group-name',
   400            job: {
   401              name: 'job-name',
   402            },
   403            reservedCPU: task1.reservedCPU + task2.reservedCPU,
   404            reservedMemory: task1.reservedMemory + task2.reservedMemory,
   405          },
   406        })
   407      );
   408  
   409      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   410  
   411      assert.ok(RecommendationCard.togglesTable.toggleAllMemory.isDisabled);
   412      assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isActive);
   413      assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent);
   414    });
   415  
   416    test('it renders diff calculations in a sentence', async function (assert) {
   417      const task1 = {
   418        name: 'jortle',
   419        reservedCPU: 150,
   420        reservedMemory: 128,
   421      };
   422  
   423      const task2 = {
   424        name: 'tortle',
   425        reservedCPU: 125,
   426        reservedMemory: 256,
   427      };
   428  
   429      this.set(
   430        'summary',
   431        new MockRecommendationSummary({
   432          recommendations: [
   433            {
   434              resource: 'CPU',
   435              stats: {},
   436              task: task1,
   437              value: 50,
   438              currentValue: task1.reservedCPU,
   439            },
   440            {
   441              resource: 'MemoryMB',
   442              stats: {},
   443              task: task1,
   444              value: 192,
   445              currentValue: task1.reservedMemory,
   446            },
   447            {
   448              resource: 'CPU',
   449              stats: {},
   450              task: task2,
   451              value: 150,
   452              currentValue: task2.reservedCPU,
   453            },
   454            {
   455              resource: 'MemoryMB',
   456              stats: {},
   457              task: task2,
   458              value: 320,
   459              currentValue: task2.reservedMemory,
   460            },
   461          ],
   462  
   463          taskGroup: {
   464            count: 10,
   465            name: 'group-name',
   466            job: {
   467              name: 'job-name',
   468              namespace: {
   469                name: 'namespace',
   470              },
   471            },
   472            reservedCPU: task1.reservedCPU + task2.reservedCPU,
   473            reservedMemory: task1.reservedMemory + task2.reservedMemory,
   474          },
   475        })
   476      );
   477  
   478      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   479  
   480      const [cpuRec1, memRec1, cpuRec2, memRec2] = this.summary.recommendations;
   481  
   482      assert.equal(
   483        RecommendationCard.narrative.trim(),
   484        'Applying the selected recommendations will save an aggregate 750 MHz of CPU and add an aggregate 1.25 GiB of memory across 10 allocations.'
   485      );
   486  
   487      this.summary.toggleRecommendation(cpuRec1);
   488      await settled();
   489  
   490      assert.equal(
   491        RecommendationCard.narrative.trim(),
   492        'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 1.25 GiB of memory across 10 allocations.'
   493      );
   494  
   495      this.summary.toggleRecommendation(memRec1);
   496      await settled();
   497  
   498      assert.equal(
   499        RecommendationCard.narrative.trim(),
   500        'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 640 MiB of memory across 10 allocations.'
   501      );
   502  
   503      this.summary.toggleRecommendation(cpuRec2);
   504      await settled();
   505  
   506      assert.equal(
   507        RecommendationCard.narrative.trim(),
   508        'Applying the selected recommendations will add an aggregate 640 MiB of memory across 10 allocations.'
   509      );
   510  
   511      this.summary.toggleRecommendation(cpuRec1);
   512      this.summary.toggleRecommendation(memRec2);
   513      await settled();
   514  
   515      assert.equal(
   516        RecommendationCard.narrative.trim(),
   517        'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.'
   518      );
   519  
   520      this.summary.toggleRecommendation(cpuRec1);
   521      await settled();
   522  
   523      assert.equal(RecommendationCard.narrative.trim(), '');
   524  
   525      this.summary.toggleRecommendation(cpuRec1);
   526      await settled();
   527  
   528      assert.equal(
   529        RecommendationCard.narrative.trim(),
   530        'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.'
   531      );
   532  
   533      this.summary.toggleRecommendation(memRec2);
   534      set(memRec2, 'value', 128);
   535      await settled();
   536  
   537      assert.equal(
   538        RecommendationCard.narrative.trim(),
   539        'Applying the selected recommendations will save an aggregate 1 GHz of CPU and 1.25 GiB of memory across 10 allocations.'
   540      );
   541    });
   542  
   543    test('it renders diff calculations in a sentence with no aggregation for one allocatio', async function (assert) {
   544      const task1 = {
   545        name: 'jortle',
   546        reservedCPU: 150,
   547        reservedMemory: 128,
   548      };
   549  
   550      const task2 = {
   551        name: 'tortle',
   552        reservedCPU: 125,
   553        reservedMemory: 256,
   554      };
   555  
   556      this.set(
   557        'summary',
   558        new MockRecommendationSummary({
   559          recommendations: [
   560            {
   561              resource: 'CPU',
   562              stats: {},
   563              task: task1,
   564              value: 50,
   565              currentValue: task1.reservedCPU,
   566            },
   567            {
   568              resource: 'MemoryMB',
   569              stats: {},
   570              task: task1,
   571              value: 192,
   572              currentValue: task1.reservedMemory,
   573            },
   574            {
   575              resource: 'CPU',
   576              stats: {},
   577              task: task2,
   578              value: 150,
   579              currentValue: task2.reservedCPU,
   580            },
   581            {
   582              resource: 'MemoryMB',
   583              stats: {},
   584              task: task2,
   585              value: 320,
   586              currentValue: task2.reservedMemory,
   587            },
   588          ],
   589  
   590          taskGroup: {
   591            count: 1,
   592            name: 'group-name',
   593            job: {
   594              name: 'job-name',
   595              namespace: {
   596                name: 'namespace',
   597              },
   598            },
   599            reservedCPU: task1.reservedCPU + task2.reservedCPU,
   600            reservedMemory: task1.reservedMemory + task2.reservedMemory,
   601          },
   602        })
   603      );
   604  
   605      await render(hbs`<Das::RecommendationCard @summary={{this.summary}} />`);
   606  
   607      assert.equal(
   608        RecommendationCard.narrative.trim(),
   609        'Applying the selected recommendations will save 75 MHz of CPU and add 128 MiB of memory.'
   610      );
   611    });
   612  });
   613  
   614  class MockRecommendationSummary {
   615    @tracked excludedRecommendations = [];
   616  
   617    constructor(attributes) {
   618      Object.assign(this, attributes);
   619    }
   620  
   621    get slug() {
   622      return `${this.taskGroup?.job?.name}/${this.taskGroup?.name}`;
   623    }
   624  
   625    @action
   626    toggleRecommendation(recommendation) {
   627      if (this.excludedRecommendations.includes(recommendation)) {
   628        this.excludedRecommendations.removeObject(recommendation);
   629      } else {
   630        this.excludedRecommendations.pushObject(recommendation);
   631      }
   632    }
   633  
   634    @action
   635    toggleAllRecommendationsForResource(resource, enabled) {
   636      if (enabled) {
   637        this.excludedRecommendations = this.excludedRecommendations.rejectBy(
   638          'resource',
   639          resource
   640        );
   641      } else {
   642        this.excludedRecommendations.pushObjects(
   643          this.recommendations.filterBy('resource', resource)
   644        );
   645      }
   646    }
   647  }