import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, settled } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import Service from '@ember/service'; import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit'; import RecommendationCardComponent from 'nomad-ui/tests/pages/components/recommendation-card'; import { create } from 'ember-cli-page-object'; const RecommendationCard = create(RecommendationCardComponent); import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { set } from '@ember/object'; module('Integration | Component | das/recommendation-card', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { const mockRouter = Service.extend({ init() { this._super(...arguments); }, urlFor(route, slug, { queryParams: { namespace } }) { return `${route}:${slug}?namespace=${namespace}`; }, }); this.owner.register('service:router', mockRouter); }); test('it renders a recommendation card', async function (assert) { assert.expect(49); const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; const task2 = { name: 'tortle', reservedCPU: 125, reservedMemory: 256, }; this.set( 'summary', new MockRecommendationSummary({ jobNamespace: 'namespace', recommendations: [ { resource: 'MemoryMB', stats: {}, task: task1, value: 192, currentValue: task1.reservedMemory, }, { resource: 'CPU', stats: {}, task: task1, value: 50, currentValue: task1.reservedCPU, }, { resource: 'CPU', stats: {}, task: task2, value: 150, currentValue: task2.reservedCPU, }, { resource: 'MemoryMB', stats: {}, task: task2, value: 320, currentValue: task2.reservedMemory, }, ], taskGroup: { count: 2, name: 'group-name', job: { name: 'job-name', namespace: { name: 'namespace', }, }, reservedCPU: task1.reservedCPU + task2.reservedCPU, reservedMemory: task1.reservedMemory + task2.reservedMemory, }, }) ); await render(hbs``); assert.equal(RecommendationCard.slug.jobName, 'job-name'); assert.equal(RecommendationCard.slug.groupName, 'group-name'); assert.equal(RecommendationCard.namespace, 'namespace'); assert.equal(RecommendationCard.totalsTable.current.cpu.text, '275 MHz'); assert.equal(RecommendationCard.totalsTable.current.memory.text, '384 MiB'); RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => { assert.equal(RecommendedCpu.text, '200 MHz'); assert.ok(RecommendedCpu.isDecrease); }); RecommendationCard.totalsTable.recommended.memory.as( (RecommendedMemory) => { assert.equal(RecommendedMemory.text, '512 MiB'); assert.ok(RecommendedMemory.isIncrease); } ); assert.equal(RecommendationCard.totalsTable.unitDiff.cpu, '-75 MHz'); assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '+128 MiB'); // Expected signal has a minus character, not a hyphen. assert.equal(RecommendationCard.totalsTable.percentDiff.cpu, '−27%'); assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+33%'); assert.equal(RecommendationCard.copyButton.text, 'job-name / group-name'); assert.ok( RecommendationCard.copyButton.clipboardText.endsWith( 'optimize.summary:job-name/group-name?namespace=namespace' ) ); assert.equal( RecommendationCard.activeTask.totalsTable.current.cpu.text, '150 MHz' ); assert.equal( RecommendationCard.activeTask.totalsTable.current.memory.text, '128 MiB' ); RecommendationCard.activeTask.totalsTable.recommended.cpu.as( (RecommendedCpu) => { assert.equal(RecommendedCpu.text, '50 MHz'); assert.ok(RecommendedCpu.isDecrease); } ); RecommendationCard.activeTask.totalsTable.recommended.memory.as( (RecommendedMemory) => { assert.equal(RecommendedMemory.text, '192 MiB'); assert.ok(RecommendedMemory.isIncrease); } ); assert.equal(RecommendationCard.activeTask.charts.length, 2); assert.equal( RecommendationCard.activeTask.charts[0].resource, 'CPU', 'CPU chart should be first when present' ); assert.ok(RecommendationCard.activeTask.cpuChart.isDecrease); assert.ok(RecommendationCard.activeTask.memoryChart.isIncrease); assert.equal(RecommendationCard.togglesTable.tasks.length, 2); await RecommendationCard.togglesTable.tasks[0].as(async (FirstTask) => { assert.equal(FirstTask.name, 'jortle'); assert.ok(FirstTask.isActive); assert.equal(FirstTask.cpu.title, 'CPU for jortle'); assert.ok(FirstTask.cpu.isActive); assert.equal(FirstTask.memory.title, 'Memory for jortle'); assert.ok(FirstTask.memory.isActive); await FirstTask.cpu.toggle(); assert.notOk(FirstTask.cpu.isActive); assert.ok(RecommendationCard.activeTask.cpuChart.isDisabled); }); assert.notOk(RecommendationCard.togglesTable.tasks[1].isActive); assert.equal(RecommendationCard.activeTask.name, 'jortle task'); RecommendationCard.totalsTable.recommended.cpu.as((RecommendedCpu) => { assert.equal(RecommendedCpu.text, '300 MHz'); assert.ok(RecommendedCpu.isIncrease); }); RecommendationCard.activeTask.totalsTable.recommended.cpu.as( (RecommendedCpu) => { assert.equal(RecommendedCpu.text, '150 MHz'); assert.ok(RecommendedCpu.isNeutral); } ); await RecommendationCard.togglesTable.toggleAllMemory.toggle(); assert.notOk(RecommendationCard.togglesTable.tasks[0].memory.isActive); assert.notOk(RecommendationCard.togglesTable.tasks[1].memory.isActive); RecommendationCard.totalsTable.recommended.memory.as( (RecommendedMemory) => { assert.equal(RecommendedMemory.text, '384 MiB'); assert.ok(RecommendedMemory.isNeutral); } ); await RecommendationCard.togglesTable.tasks[1].click(); assert.notOk(RecommendationCard.togglesTable.tasks[0].isActive); assert.ok(RecommendationCard.togglesTable.tasks[1].isActive); assert.equal(RecommendationCard.activeTask.name, 'tortle task'); assert.equal( RecommendationCard.activeTask.totalsTable.current.cpu.text, '125 MHz' ); await componentA11yAudit(this.element, assert); }); test('it doesn’t have header toggles when there’s only one task', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, }, { resource: 'MemoryMB', stats: {}, task: task1, value: 192, }, ], taskGroup: { count: 1, reservedCPU: task1.reservedCPU, reservedMemory: task1.reservedMemory, }, }) ); await render(hbs``); assert.notOk(RecommendationCard.togglesTable.toggleAllIsPresent); assert.notOk(RecommendationCard.togglesTable.toggleAllCPU.isPresent); assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isPresent); }); test('it disables the accept button when all recommendations are disabled', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, }, { resource: 'MemoryMB', stats: {}, task: task1, value: 192, }, ], taskGroup: { count: 1, reservedCPU: task1.reservedCPU, reservedMemory: task1.reservedMemory, }, }) ); await render(hbs``); await RecommendationCard.togglesTable.tasks[0].cpu.toggle(); await RecommendationCard.togglesTable.tasks[0].memory.toggle(); assert.ok(RecommendationCard.acceptButton.isDisabled); }); test('it doesn’t show a toggle or chart when there’s no recommendation for that resource', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, }, ], taskGroup: { count: 2, name: 'group-name', job: { name: 'job-name', }, reservedCPU: task1.reservedCPU, reservedMemory: task1.reservedMemory, }, }) ); await render(hbs``); assert.equal( RecommendationCard.totalsTable.recommended.memory.text, '128 MiB' ); assert.equal(RecommendationCard.totalsTable.unitDiff.memory, '0 MiB'); assert.equal(RecommendationCard.totalsTable.percentDiff.memory, '+0%'); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save an aggregate 200 MHz of CPU across 2 allocations.' ); assert.ok(RecommendationCard.togglesTable.tasks[0].memory.isDisabled); assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent); }); test('it disables a resource’s toggle all toggle when there are no recommendations for it', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; const task2 = { name: 'tortle', reservedCPU: 150, reservedMemory: 128, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, }, { resource: 'CPU', stats: {}, task: task2, value: 50, }, ], taskGroup: { count: 2, name: 'group-name', job: { name: 'job-name', }, reservedCPU: task1.reservedCPU + task2.reservedCPU, reservedMemory: task1.reservedMemory + task2.reservedMemory, }, }) ); await render(hbs``); assert.ok(RecommendationCard.togglesTable.toggleAllMemory.isDisabled); assert.notOk(RecommendationCard.togglesTable.toggleAllMemory.isActive); assert.notOk(RecommendationCard.activeTask.memoryChart.isPresent); }); test('it renders diff calculations in a sentence', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; const task2 = { name: 'tortle', reservedCPU: 125, reservedMemory: 256, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, currentValue: task1.reservedCPU, }, { resource: 'MemoryMB', stats: {}, task: task1, value: 192, currentValue: task1.reservedMemory, }, { resource: 'CPU', stats: {}, task: task2, value: 150, currentValue: task2.reservedCPU, }, { resource: 'MemoryMB', stats: {}, task: task2, value: 320, currentValue: task2.reservedMemory, }, ], taskGroup: { count: 10, name: 'group-name', job: { name: 'job-name', namespace: { name: 'namespace', }, }, reservedCPU: task1.reservedCPU + task2.reservedCPU, reservedMemory: task1.reservedMemory + task2.reservedMemory, }, }) ); await render(hbs``); const [cpuRec1, memRec1, cpuRec2, memRec2] = this.summary.recommendations; assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save an aggregate 750 MHz of CPU and add an aggregate 1.25 GiB of memory across 10 allocations.' ); this.summary.toggleRecommendation(cpuRec1); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 1.25 GiB of memory across 10 allocations.' ); this.summary.toggleRecommendation(memRec1); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will add an aggregate 250 MHz of CPU and 640 MiB of memory across 10 allocations.' ); this.summary.toggleRecommendation(cpuRec2); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will add an aggregate 640 MiB of memory across 10 allocations.' ); this.summary.toggleRecommendation(cpuRec1); this.summary.toggleRecommendation(memRec2); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.' ); this.summary.toggleRecommendation(cpuRec1); await settled(); assert.equal(RecommendationCard.narrative.trim(), ''); this.summary.toggleRecommendation(cpuRec1); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save an aggregate 1 GHz of CPU across 10 allocations.' ); this.summary.toggleRecommendation(memRec2); set(memRec2, 'value', 128); await settled(); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save an aggregate 1 GHz of CPU and 1.25 GiB of memory across 10 allocations.' ); }); test('it renders diff calculations in a sentence with no aggregation for one allocatio', async function (assert) { const task1 = { name: 'jortle', reservedCPU: 150, reservedMemory: 128, }; const task2 = { name: 'tortle', reservedCPU: 125, reservedMemory: 256, }; this.set( 'summary', new MockRecommendationSummary({ recommendations: [ { resource: 'CPU', stats: {}, task: task1, value: 50, currentValue: task1.reservedCPU, }, { resource: 'MemoryMB', stats: {}, task: task1, value: 192, currentValue: task1.reservedMemory, }, { resource: 'CPU', stats: {}, task: task2, value: 150, currentValue: task2.reservedCPU, }, { resource: 'MemoryMB', stats: {}, task: task2, value: 320, currentValue: task2.reservedMemory, }, ], taskGroup: { count: 1, name: 'group-name', job: { name: 'job-name', namespace: { name: 'namespace', }, }, reservedCPU: task1.reservedCPU + task2.reservedCPU, reservedMemory: task1.reservedMemory + task2.reservedMemory, }, }) ); await render(hbs``); assert.equal( RecommendationCard.narrative.trim(), 'Applying the selected recommendations will save 75 MHz of CPU and add 128 MiB of memory.' ); }); }); class MockRecommendationSummary { @tracked excludedRecommendations = []; constructor(attributes) { Object.assign(this, attributes); } get slug() { return `${this.taskGroup?.job?.name}/${this.taskGroup?.name}`; } @action toggleRecommendation(recommendation) { if (this.excludedRecommendations.includes(recommendation)) { this.excludedRecommendations.removeObject(recommendation); } else { this.excludedRecommendations.pushObject(recommendation); } } @action toggleAllRecommendationsForResource(resource, enabled) { if (enabled) { this.excludedRecommendations = this.excludedRecommendations.rejectBy( 'resource', resource ); } else { this.excludedRecommendations.pushObjects( this.recommendations.filterBy('resource', resource) ); } } }