2020-10-30 21:23:59 +00:00
|
|
|
import { findAll } from '@ember/test-helpers';
|
2020-10-12 05:58:44 +00:00
|
|
|
import { module, test } from 'qunit';
|
|
|
|
import { setupRenderingTest } from 'ember-qunit';
|
|
|
|
import hbs from 'htmlbars-inline-precompile';
|
|
|
|
import { create } from 'ember-cli-page-object';
|
|
|
|
import sinon from 'sinon';
|
|
|
|
import faker from 'nomad-ui/mirage/faker';
|
2020-10-30 21:23:59 +00:00
|
|
|
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
|
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
2020-10-12 05:58:44 +00:00
|
|
|
import topoVisNodePageObject from 'nomad-ui/tests/pages/components/topo-viz/node';
|
|
|
|
|
|
|
|
const TopoVizNode = create(topoVisNodePageObject());
|
|
|
|
|
|
|
|
const nodeGen = (name, datacenter, memory, cpu, flags = {}) => ({
|
|
|
|
datacenter,
|
|
|
|
memory,
|
|
|
|
cpu,
|
|
|
|
isSelected: !!flags.isSelected,
|
|
|
|
node: {
|
|
|
|
name,
|
|
|
|
isEligible: flags.isEligible || flags.isEligible == null,
|
|
|
|
isDraining: !!flags.isDraining,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const allocGen = (node, memory, cpu, isSelected) => ({
|
|
|
|
memory,
|
|
|
|
cpu,
|
|
|
|
isSelected,
|
|
|
|
memoryPercent: memory / node.memory,
|
|
|
|
cpuPercent: cpu / node.cpu,
|
|
|
|
allocation: {
|
|
|
|
id: faker.random.uuid(),
|
|
|
|
isScheduled: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const props = overrides => ({
|
|
|
|
isDense: false,
|
|
|
|
heightScale: () => 50,
|
|
|
|
onAllocationSelect: sinon.spy(),
|
|
|
|
onNodeSelect: sinon.spy(),
|
2020-10-30 21:23:59 +00:00
|
|
|
onAllocationFocus: sinon.spy(),
|
|
|
|
onAllocationBlur: sinon.spy(),
|
2020-10-12 05:58:44 +00:00
|
|
|
...overrides,
|
|
|
|
});
|
|
|
|
|
|
|
|
module('Integration | Component | TopoViz::Node', function(hooks) {
|
|
|
|
setupRenderingTest(hooks);
|
2020-10-30 21:23:59 +00:00
|
|
|
setupMirage(hooks);
|
2020-10-12 05:58:44 +00:00
|
|
|
|
|
|
|
const commonTemplate = hbs`
|
|
|
|
<TopoViz::Node
|
|
|
|
@node={{this.node}}
|
|
|
|
@isDense={{this.isDense}}
|
|
|
|
@heightScale={{this.heightScale}}
|
|
|
|
@onAllocationSelect={{this.onAllocationSelect}}
|
2020-10-30 21:23:59 +00:00
|
|
|
@onAllocationFocus={{this.onAllocationFocus}}
|
|
|
|
@onAllocationBlur={{this.onAllocationBlur}}
|
2020-10-12 05:58:44 +00:00
|
|
|
@onNodeSelect={{this.onNodeSelect}} />
|
|
|
|
`;
|
|
|
|
|
|
|
|
test('presents as a div with a label and an svg with CPU and memory rows', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.isPresent);
|
|
|
|
assert.ok(TopoVizNode.memoryRects.length);
|
|
|
|
assert.ok(TopoVizNode.cpuRects.length);
|
|
|
|
|
|
|
|
await componentA11yAudit(this.element, assert);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('the label contains aggregate information about the node', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.label.includes(node.node.name));
|
|
|
|
assert.ok(TopoVizNode.label.includes(`${this.node.allocations.length} Allocs`));
|
|
|
|
assert.ok(TopoVizNode.label.includes(`${this.node.memory} MiB`));
|
2021-01-26 18:53:26 +00:00
|
|
|
assert.ok(TopoVizNode.label.includes(`${this.node.cpu} MHz`));
|
2020-10-12 05:58:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
test('the status icon indicates when the node is draining', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000, { isDraining: true });
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.statusIcon.includes('icon-is-clock-outline'));
|
|
|
|
assert.equal(TopoVizNode.statusIconLabel, 'Client is draining');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('the status icon indicates when the node is ineligible for scheduling', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000, { isEligible: false });
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.statusIcon.includes('icon-is-lock-closed'));
|
|
|
|
assert.equal(TopoVizNode.statusIconLabel, 'Client is ineligible');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when isDense is false, clicking the node does nothing', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
isDense: false,
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
await TopoVizNode.selectNode();
|
|
|
|
|
|
|
|
assert.notOk(TopoVizNode.nodeIsInteractive);
|
|
|
|
assert.notOk(this.onNodeSelect.called);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when isDense is true, clicking the node calls onNodeSelect', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
isDense: true,
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
await TopoVizNode.selectNode();
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.nodeIsInteractive);
|
|
|
|
assert.ok(this.onNodeSelect.called);
|
|
|
|
assert.ok(this.onNodeSelect.calledWith(this.node));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('the node gets the is-selected class when the node is selected', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000, { isSelected: true });
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
isDense: true,
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(TopoVizNode.nodeIsSelected);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('the node gets its height form the @heightScale arg', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
const height = 50;
|
|
|
|
const heightSpy = sinon.spy();
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
heightScale: (...args) => {
|
|
|
|
heightSpy(...args);
|
|
|
|
return height;
|
|
|
|
},
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.ok(heightSpy.called);
|
|
|
|
assert.ok(heightSpy.calledWith(this.node.memory));
|
|
|
|
assert.equal(TopoVizNode.memoryRects[0].height, `${height}px`);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('each allocation gets a memory rect and a cpu rect', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
assert.equal(TopoVizNode.memoryRects.length, this.node.allocations.length);
|
|
|
|
assert.equal(TopoVizNode.cpuRects.length, this.node.allocations.length);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('each allocation is sized according to its percentage of utilization', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(hbs`
|
|
|
|
<div style="width:100px">
|
|
|
|
<TopoViz::Node
|
|
|
|
@node={{this.node}}
|
|
|
|
@isDense={{this.isDense}}
|
|
|
|
@heightScale={{this.heightScale}}
|
|
|
|
@onAllocationSelect={{this.onAllocationSelect}}
|
|
|
|
@onNodeSelect={{this.onNodeSelect}} />
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
|
|
|
|
// Remove the width of the padding and the label from the SVG width
|
|
|
|
const width = 100 - 5 - 5 - 20;
|
|
|
|
this.node.allocations.forEach((alloc, index) => {
|
|
|
|
const memWidth = alloc.memoryPercent * width - (index === 0 ? 0.5 : 1);
|
|
|
|
const cpuWidth = alloc.cpuPercent * width - (index === 0 ? 0.5 : 1);
|
|
|
|
assert.equal(TopoVizNode.memoryRects[index].width, `${memWidth}px`);
|
|
|
|
assert.equal(TopoVizNode.cpuRects[index].width, `${cpuWidth}px`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('clicking either the memory or cpu rect for an allocation will call onAllocationSelect', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
await TopoVizNode.memoryRects[0].select();
|
2020-10-30 21:23:59 +00:00
|
|
|
assert.equal(this.onAllocationSelect.callCount, 1);
|
2020-10-12 05:58:44 +00:00
|
|
|
assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[0]));
|
|
|
|
|
|
|
|
await TopoVizNode.cpuRects[0].select();
|
2020-10-30 21:23:59 +00:00
|
|
|
assert.equal(this.onAllocationSelect.callCount, 2);
|
2020-10-12 05:58:44 +00:00
|
|
|
|
|
|
|
await TopoVizNode.cpuRects[1].select();
|
2020-10-30 21:23:59 +00:00
|
|
|
assert.equal(this.onAllocationSelect.callCount, 3);
|
2020-10-12 05:58:44 +00:00
|
|
|
assert.ok(this.onAllocationSelect.calledWith(this.node.allocations[1]));
|
|
|
|
|
|
|
|
await TopoVizNode.memoryRects[1].select();
|
2020-10-30 21:23:59 +00:00
|
|
|
assert.equal(this.onAllocationSelect.callCount, 4);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('hovering over a memory or cpu rect for an allocation will call onAllocationFocus', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
await TopoVizNode.memoryRects[0].hover();
|
|
|
|
assert.equal(this.onAllocationFocus.callCount, 1);
|
|
|
|
assert.equal(
|
|
|
|
this.onAllocationFocus.getCall(0).args[0].allocation,
|
|
|
|
this.node.allocations[0].allocation
|
|
|
|
);
|
|
|
|
assert.equal(this.onAllocationFocus.getCall(0).args[1], findAll('[data-test-memory-rect]')[0]);
|
|
|
|
|
|
|
|
await TopoVizNode.cpuRects[1].hover();
|
|
|
|
assert.equal(this.onAllocationFocus.callCount, 2);
|
|
|
|
assert.equal(
|
|
|
|
this.onAllocationFocus.getCall(1).args[0].allocation,
|
|
|
|
this.node.allocations[1].allocation
|
|
|
|
);
|
|
|
|
assert.equal(this.onAllocationFocus.getCall(1).args[1], findAll('[data-test-cpu-rect]')[1]);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('leaving the entire node will call onAllocationBlur, which allows for the tooltip transitions', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [allocGen(node, 100, 100), allocGen(node, 250, 250)],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
await TopoVizNode.memoryRects[0].hover();
|
|
|
|
assert.equal(this.onAllocationFocus.callCount, 1);
|
|
|
|
assert.equal(this.onAllocationBlur.callCount, 0);
|
|
|
|
|
|
|
|
await TopoVizNode.memoryRects[0].mouseleave();
|
|
|
|
assert.equal(this.onAllocationBlur.callCount, 0);
|
|
|
|
|
|
|
|
await TopoVizNode.mouseout();
|
|
|
|
assert.equal(this.onAllocationBlur.callCount, 1);
|
2020-10-12 05:58:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
test('allocations are sorted by smallest to largest delta of memory to cpu percent utilizations', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
|
|
|
|
const evenAlloc = allocGen(node, 100, 100);
|
|
|
|
const mediumMemoryAlloc = allocGen(node, 200, 150);
|
|
|
|
const largeMemoryAlloc = allocGen(node, 300, 50);
|
|
|
|
const mediumCPUAlloc = allocGen(node, 150, 200);
|
|
|
|
const largeCPUAlloc = allocGen(node, 50, 300);
|
|
|
|
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [
|
|
|
|
largeCPUAlloc,
|
|
|
|
mediumCPUAlloc,
|
|
|
|
evenAlloc,
|
|
|
|
mediumMemoryAlloc,
|
|
|
|
largeMemoryAlloc,
|
|
|
|
],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
|
|
|
|
const expectedOrder = [
|
|
|
|
evenAlloc,
|
|
|
|
mediumCPUAlloc,
|
|
|
|
mediumMemoryAlloc,
|
|
|
|
largeCPUAlloc,
|
|
|
|
largeMemoryAlloc,
|
|
|
|
];
|
|
|
|
expectedOrder.forEach((alloc, index) => {
|
|
|
|
assert.equal(TopoVizNode.memoryRects[index].id, alloc.allocation.id);
|
|
|
|
assert.equal(TopoVizNode.cpuRects[index].id, alloc.allocation.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('when there are no allocations, a "no allocations" note is shown', async function(assert) {
|
|
|
|
const node = nodeGen('Node One', 'dc1', 1000, 1000);
|
|
|
|
this.setProperties(
|
|
|
|
props({
|
|
|
|
node: {
|
|
|
|
...node,
|
|
|
|
allocations: [],
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.render(commonTemplate);
|
|
|
|
assert.equal(TopoVizNode.emptyMessage, 'Empty Client');
|
|
|
|
});
|
|
|
|
});
|