Skip to content
This repository has been archived by the owner on May 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #330 from jaycee/test-love-2-machines
Browse files Browse the repository at this point in the history
app/widgets/machine-panel-view.js 
* ```_smartUpdateList``` has been refactored, moving its two responsibilities--
  adding and removing tokens as necessary--into two methods. These methods are
  easier to test, and a bit clearer to read.

tests/test_machine_view_panel_view.js
* Unit tests for all inputs and all expected behavior of the components of
  _smartUpdateList have been added.
* Unit tests for _updateMachine and _renderMachineToken have been added

Note to reviewers: machine tokens are also tested in their own file, which has
not been updated. Additionally, invisible in the diff, but available in the
full file, machine column has quite extensive functional testing; I've only
added units to more directly tests private methods that were only implicitly
tested.
  • Loading branch information
jujugui committed May 20, 2014
2 parents 662d449 + e9e4f9d commit 1417c1f
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 33 deletions.
94 changes: 61 additions & 33 deletions app/widgets/machine-view-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,13 @@ YUI.add('machine-view-panel', function(Y) {
* @method _bindEvents
*/
_bindEvents: function() {
this.addEvent(
this.get('db').machines.after(['add', 'remove', '*:change'],
this._onMachinesChange, this)
var db = this.get('db');
this.addEvent(db.machines.after(
['add', 'remove', '*:change'], this._onMachinesChange, this)
);

this.addEvent(
this.get('db').units.after(['add', 'remove', '*:change'],
this._onUnitsChange, this)
);
this.addEvent(db.units.after(
['add', 'remove', '*:change'], this._onUnitsChange, this));

this.on('*:unit-token-drag-start', this._showDraggingUI, this);
this.on('*:unit-token-drag-end', this._hideDraggingUI, this);
Expand Down Expand Up @@ -372,14 +370,15 @@ YUI.add('machine-view-panel', function(Y) {
* @param {Node} list the list node to append the machine to.
*/
_renderMachineToken: function(machineOrId) {
var machine;
var db = this.get('db'),
machine;
if (typeof machineOrId === 'string') {
machine = this.get('db').machines.getById(machineOrId);
machine = db.machines.getById(machineOrId);
} else {
machine = machineOrId;
}
var node = Y.Node.create('<li></li>'),
units = this.get('db').units.filterByMachine(machine.id, true);
units = db.units.filterByMachine(machine.id, true);
this._updateMachineWithUnitData(machine, units);
var token = new views.MachineToken({
container: node,
Expand Down Expand Up @@ -424,28 +423,45 @@ YUI.add('machine-view-panel', function(Y) {
},

/**
* A helper function that handles intelligently updating the lists in
* the machine view columns. When an update comes in, we don't want to
* re-render the entire view, just the items in the list that have
* changed. The logic below handles that and uses the render and
* cleanup callbacks to allow for column-specific logic.
*
* @method _smartUpdateList
* @param {Array} models The models that are bound to the list
* @param {NodeList} list The DOM list
* @param {Function} render A callback to handle rendering one item in
* the list
* @param {Function} cleanup Any other changes that need to occur after
* an update in the list, i.e., clearing the
* container column
A helper function that handles intelligently updating the lists in
the machine view columns. When an update comes in, we don't want to
re-render the entire view, just the items in the list that have
changed. The logic below handles that and uses the render and
cleanup callbacks to allow for column-specific logic.
@method _smartUpdateList
@param {Array} models The models that are bound to the list
@param {NodeList} list The DOM list
@param {Function} renderFn A callback to handle rendering one item
in the list
@param {Function} cleanupFn Any other changes that need to occur
after an update in the list, i.e., clearing the
container column
*/
_smartUpdateList: function(models, list, render, cleanup) {
var exists, newElement;
if (!models || !list) {
return;
}
var elements = list.all('li');
var newElements = this._addNewTokens(models, elements, render);
newElements.forEach(function(newToken) {
list.append(newToken);
});
this._removeOldTokens(models, elements, cleanup);
},

/**
Creates new tokens for any models not yet found in the DOM.
@method _addNewTokens
@param {Array} models The list of machine models
@param {Y.NodeList} elements The list of tokens in the DOM
@param {function} renderFn The function to create the rendered token.
*/
_addNewTokens: function(models, elements, renderFn) {
var list = [],
exists,
newElement;
models.forEach(function(model) {
exists = elements.some(function(element) {
// If the model already exists in the dom, mark it as such.
Expand All @@ -454,33 +470,45 @@ YUI.add('machine-view-panel', function(Y) {
return true;
}
});

if (!exists) {
// If the model does not exist in the dom, render the token.
newElement = render(model);
list.append(newElement);
newElement = renderFn(model);
list.push(newElement);
newElement.setData('exists', true);
}
}, this);
return list;
},

/**
Removes tokens for items that no longer have a corresponding model.
@method _removeOldTokens
@param {Array} models The list of machine models
@param {Y.NodeList} elements The list of tokens in the DOM
@param {function} cleanupFn Option cleanup function
*/
_removeOldTokens: function(models, elements, cleanupFn) {
elements.each(function(element) {
if (!element.getData('exists')) {
// If the element exists in the dom, but not in the model
// list then it must have been removed from the DB, so remove it
// from the dom.
var token = element.one('.token');
if (token && token.hasClass('active')) {
if (token && token.hasClass('active') && cleanupFn) {
// If the selected model was removed then stop showing
// its containers.
if (typeof cleanup === 'function') {
cleanup();
if (cleanupFn) {
cleanupFn();
}
}
element.remove();
} else if (models.length === 0) {
element.remove();
if (typeof cleanup === 'function') {
cleanup();
if (cleanupFn) {
cleanupFn();
}
element.remove();
} else {
// Clean up the 'exists' flag for the next loop through
// the nodes.
Expand Down
166 changes: 166 additions & 0 deletions test/test_machine_view_panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,88 @@ describe('machine view panel view', function() {
'container').one('.label').get('text'), label);
});

describe('_smartUpdateList', function() {
// _smartUpdateList use two private methods, _addNewTokens and
// _removeOldTokens that do all the work--we can just test those.
it('can determine if a model is new and needs token', function() {
var models = [{id: 1}];
var newTokens = view._addNewTokens(models, [], function(model) {
return Y.Node.create('<li/>');
});
assert.equal(
newTokens.length, 1,
'Did not received expected number of tokens');
assert.equal(
newTokens[0].getData('exists'), true,
'New element not marked with "exists"');
});

it('can determine if a model already has a token', function() {
var models = [{id: 1}],
element = Y.Node.create('<li data-id="1"></li>');
var newTokens = view._addNewTokens(models, [element], function(model) {
return Y.Node.create('<li/>');
});
assert.equal(
newTokens.length, 0,
'Received tokens when no new tokens should have been created');
assert.equal(
element.getData('exists'), true,
'Element not marked with "exists"');
});

it('can determine if a token has no model and remove it', function() {
container.append(Y.Node.create('<li data-id="2"></li>'));
var models = [{id: 1}],
elements = container.all('li');
assert.equal(elements.size(), 1);
view._removeOldTokens(models, elements);
assert.equal(container.all('li').size(), 0, 'Element not removed.');
});

it('can determine if there are no models and clear nodes', function() {
container.append(Y.Node.create('<li></li>'));
var models = [],
elements = container.all('li');
// Make sure the element has "exists" so we test the models condition.
elements.each(function(element) {
element.setData('exists', true);
});
assert.equal(elements.size(), 1);
view._removeOldTokens(models, elements);
assert.equal(container.all('li').size(), 0, 'Element not removed.');
});

it('calls a cleanup function if an "active" token is removed', function() {
container.append(Y.Node.create('<li><span class="token active"/></li>'));
var models = [{id: 1}],
elements = container.all('li'),
cleanup = false;
assert.equal(elements.size(), 1);
var cleanupFn = function() {
cleanup = true;
};
view._removeOldTokens(models, elements, cleanupFn);
assert.equal(cleanup, true, 'Cleanup function not called.');
});

it('calls a cleanup function if there are no models', function() {
container.append(Y.Node.create('<li></li>'));
var models = [],
elements = container.all('li'),
cleanup = false;
// Make sure the element has "exists" so we test the models condition.
elements.each(function(element) {
element.setData('exists', true);
});
var cleanupFn = function() {
cleanup = true;
};
view._removeOldTokens(models, elements, cleanupFn);
assert.equal(cleanup, true, 'Cleanup function not called.');
});
});

describe('token drag and drop', function() {
beforeEach(function() {
view.set('env', {
Expand Down Expand Up @@ -321,6 +403,61 @@ describe('machine view panel view', function() {


describe('machine column', function() {
it('can create machine tokens from a machine', function() {
var updateStub = utils.makeStubMethod(
view, '_updateMachineWithUnitData');
this._cleanups.push(updateStub.reset);
var db = view.get('db'),
filterStub = utils.makeStubMethod(db.units, 'filterByMachine', []),
getByIdStub = utils.makeStubMethod(db.machines, 'getById', machine);
this._cleanups.push(filterStub.reset);
this._cleanups.push(getByIdStub.reset);
var rendered = false,
target;
var viewStub = utils.makeStubMethod(views, 'MachineToken', {
render: function() { rendered = true; },
addTarget: function(t) { target = t; }
});
this._cleanups.push(viewStub.reset);
view._renderMachineToken(machine);

assert.equal(updateStub.calledOnce(), true);
assert.equal(viewStub.calledOnce(), true);
// GetById should not be called, as we have provided a machine.
assert.equal(getByIdStub.callCount(), 0);
// Verify token is rendered and has the view added as a target.
assert.equal(rendered, true);
assert.equal(target, view);
});

it('can create machine tokens from the ID as a string', function() {
var updateStub = utils.makeStubMethod(
view, '_updateMachineWithUnitData');
this._cleanups.push(updateStub.reset);
var db = view.get('db'),
filterStub = utils.makeStubMethod(db.units, 'filterByMachine', []),
getByIdStub = utils.makeStubMethod(db.machines, 'getById', machine);
this._cleanups.push(filterStub.reset);
this._cleanups.push(getByIdStub.reset);
var rendered = false,
target;
var viewStub = utils.makeStubMethod(views, 'MachineToken', {
render: function() { rendered = true; },
addTarget: function(t) { target = t; }
});
this._cleanups.push(viewStub.reset);
view._renderMachineToken('1');

assert.equal(updateStub.calledOnce(), true);
assert.equal(viewStub.calledOnce(), true);
// GetById should be called, so we can look up the machine.
assert.equal(getByIdStub.calledOnce(), 1);
assert.equal(getByIdStub.lastArguments()[0], '1');
// Verify token is rendered and has the view added as a target.
assert.equal(rendered, true);
assert.equal(target, view);
});

it('should render a list of machines', function() {
view.render();
var list = container.all('.machines .content li');
Expand All @@ -340,6 +477,31 @@ describe('machine view panel view', function() {
assert.equal(view._machinesHeader.get(
'container').one('.label').get('text'), label);
});

it('can update a machine via an object', function() {
machine = {id: 1};
container = view.get('container');
var oneStub = utils.makeStubMethod(container, 'one', null);
this._cleanups.push(oneStub.reset);
view._updateMachine(machine);
assert.equal(
'.machines .content .machine-token[data-id="1"]',
oneStub.lastArguments()[0] ,
'Selector created with wrong id');
});

it('can update a machine via a string', function() {
machine = '1';
container = view.get('container');
var oneStub = utils.makeStubMethod(container, 'one', null);
this._cleanups.push(oneStub.reset);
view._updateMachine(machine);
assert.equal(
'.machines .content .machine-token[data-id="1"]',
oneStub.lastArguments()[0] ,
'Selector created with wrong id');
});

/// XXX Jeff May 15 2014 - drop handlers no longer update UI. Fix once
// handlers update the UI.
it.skip('updates and re-renders a specific machine', function() {
Expand All @@ -355,6 +517,7 @@ describe('machine view panel view', function() {
assert.equal(node.all('.service-icons .unit').size(),
machine.units.length, 'icons not updated along with units');
});

/// XXX Jeff May 15 2014 - drop handlers no longer update UI. Fix once
// handlers update the UI.
it.skip('should add new tokens when machines are added', function() {
Expand Down Expand Up @@ -397,6 +560,7 @@ describe('machine view panel view', function() {
assert.equal(deletedItem, null,
'found the deleted machine still in the list');
});

/// XXX Jeff May 15 2014 - drop handlers no longer update UI. Fix once
// handlers update the UI.
it.skip('should re-render token when machine is updated', function() {
Expand Down Expand Up @@ -485,6 +649,7 @@ describe('machine view panel view', function() {
render: function() { rendered = true; },
addTarget: function(t) { target = t; }
});
this._cleanups.push(viewStub.reset);
var containerParent = utils.makeContainer(this, 'machine-view-panel'),
container = {};
view._createContainerToken(containerParent, container);
Expand All @@ -510,6 +675,7 @@ describe('machine view panel view', function() {
render: function() { rendered = true; },
addTarget: function(t) { target = t; }
});
this._cleanups.push(viewStub.reset);
var containerParent = utils.makeContainer(this, 'machine-view-panel'),
units = [{}],
container = {};
Expand Down

0 comments on commit 1417c1f

Please sign in to comment.