Skip to content

Commit

Permalink
Update specs to match Knockout 2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
mbest committed May 25, 2012
1 parent 7e17483 commit e493625
Show file tree
Hide file tree
Showing 22 changed files with 467 additions and 3,415 deletions.
136 changes: 135 additions & 1 deletion spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,37 @@ describe('Binding attribute syntax', {
value_of(testNode).should_contain_text("Inner value");
},

'Should be able to extend a binding context, adding new custom properties, without mutating the original binding context': function() {
ko.bindingHandlers.addCustomProperty = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element);
return { controlsDescendantBindings : true };
}
};
testNode.innerHTML = "<div data-bind='with: sub'><div data-bind='addCustomProperty: true'><div data-bind='text: $customProp'></div></div></div>";
var vm = { sub: {} };
ko.applyBindings(vm, testNode);
value_of(testNode).should_contain_text("my value");
value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$customProp).should_be("my value");
value_of(ko.contextFor(testNode.childNodes[0].childNodes[0]).$customProp).should_be(undefined); // Should not affect original binding context

// vale of $data and $parent should be unchanged in extended context
value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data).should_be(vm.sub);
value_of(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$parent).should_be(vm);
},

'Binding contexts should inherit any custom properties from ancestor binding contexts': function() {
ko.bindingHandlers.addCustomProperty = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.applyBindingsToDescendants(bindingContext.extend({ '$customProp': 'my value' }), element);
return { controlsDescendantBindings : true };
}
};
testNode.innerHTML = "<div data-bind='addCustomProperty: true'><div data-bind='with: true'><div data-bind='text: $customProp'></div></div></div>";
ko.applyBindings(null, testNode);
value_of(testNode).should_contain_text("my value");
},

'Should be able to retrieve the binding context associated with any node': function() {
testNode.innerHTML = "<div><div data-bind='text: name'></div></div>";
ko.applyBindings({ name: 'Bert' }, testNode.childNodes[0]);
Expand Down Expand Up @@ -238,6 +269,109 @@ describe('Binding attribute syntax', {
value_of(didThrow).should_be(true);
},

'Should be able to set a custom binding to use containerless binding': function() {
var initCalls = 0;
ko.bindingHandlers.test = { init: function () { initCalls++ } };
ko.virtualElements.allowedBindings['test'] = true;

testNode.innerHTML = "Hello <!-- ko test: false -->Some text<!-- /ko --> Goodbye"
ko.applyBindings(null, testNode);

value_of(initCalls).should_be(1);
value_of(testNode).should_contain_text("Hello Some text Goodbye");
},

'Should be able to access virtual children in custom containerless binding': function() {
var countNodes = 0;
ko.bindingHandlers.test = {
init: function (element, valueAccessor) {
// Counts the number of virtual children, and overwrites the text contents of any text nodes
for (var node = ko.virtualElements.firstChild(element); node; node = ko.virtualElements.nextSibling(node)) {
countNodes++;
if (node.nodeType === 3)
node.data = 'new text';
}
}
};
ko.virtualElements.allowedBindings['test'] = true;

testNode.innerHTML = "Hello <!-- ko test: false -->Some text<!-- /ko --> Goodbye"
ko.applyBindings(null, testNode);

value_of(countNodes).should_be(1);
value_of(testNode).should_contain_text("Hello new text Goodbye");
},

'Should only bind containerless binding once inside template': function() {
var initCalls = 0;
ko.bindingHandlers.test = { init: function () { initCalls++ } };
ko.virtualElements.allowedBindings['test'] = true;

testNode.innerHTML = "Hello <!-- ko if: true --><!-- ko test: false -->Some text<!-- /ko --><!-- /ko --> Goodbye"
ko.applyBindings(null, testNode);

value_of(initCalls).should_be(1);
value_of(testNode).should_contain_text("Hello Some text Goodbye");
},

'Should automatically bind virtual descendants of containerless markers if no binding controlsDescendantBindings': function() {
testNode.innerHTML = "Hello <!-- ko dummy: false --><span data-bind='text: \"WasBound\"'>Some text</span><!-- /ko --> Goodbye";
ko.applyBindings(null, testNode);
value_of(testNode).should_contain_text("Hello WasBound Goodbye");
},

'Should be able to set and access correct context in custom containerless binding': function() {
ko.bindingHandlers.bindChildrenWithCustomContext = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var innerContext = bindingContext.createChildContext({ myCustomData: 123 });
ko.applyBindingsToDescendants(innerContext, element);
return { 'controlsDescendantBindings': true };
}
};
ko.virtualElements.allowedBindings['bindChildrenWithCustomContext'] = true;

testNode.innerHTML = "Hello <!-- ko bindChildrenWithCustomContext: true --><div>Some text</div><!-- /ko --> Goodbye"
ko.applyBindings(null, testNode);

value_of(ko.dataFor(testNode.childNodes[2]).myCustomData).should_be(123);
},

'Should be able to set and access correct context in nested containerless binding': function() {
delete ko.bindingHandlers.nonexistentHandler;
ko.bindingHandlers.bindChildrenWithCustomContext = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var innerContext = bindingContext.createChildContext({ myCustomData: 123 });
ko.applyBindingsToDescendants(innerContext, element);
return { 'controlsDescendantBindings': true };
}
};

testNode.innerHTML = "Hello <div data-bind='bindChildrenWithCustomContext: true'><!-- ko nonexistentHandler: 123 --><div>Some text</div><!-- /ko --></div> Goodbye"
ko.applyBindings(null, testNode);

value_of(ko.dataFor(testNode.childNodes[1].childNodes[0]).myCustomData).should_be(123);
value_of(ko.dataFor(testNode.childNodes[1].childNodes[1]).myCustomData).should_be(123);
},

'Should be able to access custom context variables in child context': function() {
ko.bindingHandlers.bindChildrenWithCustomContext = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var innerContext = bindingContext.createChildContext({ myCustomData: 123 });
innerContext.customValue = 'xyz';
ko.applyBindingsToDescendants(innerContext, element);
return { 'controlsDescendantBindings': true };
}
};

testNode.innerHTML = "Hello <div data-bind='bindChildrenWithCustomContext: true'><!-- ko with: myCustomData --><div>Some text</div><!-- /ko --></div> Goodbye"
ko.applyBindings(null, testNode);

value_of(ko.contextFor(testNode.childNodes[1].childNodes[0]).customValue).should_be('xyz');
value_of(ko.dataFor(testNode.childNodes[1].childNodes[1])).should_be(123);
value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parent.myCustomData).should_be(123);
value_of(ko.contextFor(testNode.childNodes[1].childNodes[1]).$parentContext.customValue).should_be('xyz');
},

'Should not reinvoke init for notifications triggered during first evaluation': function () {
var observable = ko.observable('A');
var initCalls = 0;
Expand Down Expand Up @@ -287,4 +421,4 @@ describe('Binding attribute syntax', {
ko.applyBindings({ myObservable: observable }, testNode);
value_of(hasUpdatedSecondBinding).should_be(true);
}
});
});
107 changes: 104 additions & 3 deletions spec/defaultBindingsBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,40 @@ describe('Binding: Value', {
ko.utils.triggerEvent(testNode.childNodes[0], "change");
value_of(typeof observable()).should_be("number");
value_of(observable()).should_be(20);
},

'On IE, should respond exactly once to "propertychange" followed by "blur" or "change" or both': function() {
var isIE = navigator.userAgent.indexOf("MSIE") >= 0;

if (isIE) {
var myobservable = new ko.observable(123).extend({ notify: 'always' });
var numUpdates = 0;
myobservable.subscribe(function() { numUpdates++ });
testNode.innerHTML = "<input data-bind='value:someProp' />";
ko.applyBindings({ someProp: myobservable }, testNode);

// First try change then blur
testNode.childNodes[0].value = "some user-entered value";
ko.utils.triggerEvent(testNode.childNodes[0], "propertychange");
ko.utils.triggerEvent(testNode.childNodes[0], "change");
value_of(myobservable()).should_be("some user-entered value");
ko.processAllDeferredUpdates();
value_of(numUpdates).should_be(1);
ko.utils.triggerEvent(testNode.childNodes[0], "blur");
ko.processAllDeferredUpdates();
value_of(numUpdates).should_be(1);

// Now try blur then change
testNode.childNodes[0].value = "different user-entered value";
ko.utils.triggerEvent(testNode.childNodes[0], "propertychange");
ko.utils.triggerEvent(testNode.childNodes[0], "blur");
value_of(myobservable()).should_be("different user-entered value");
ko.processAllDeferredUpdates();
value_of(numUpdates).should_be(2);
ko.utils.triggerEvent(testNode.childNodes[0], "change");
ko.processAllDeferredUpdates();
value_of(numUpdates).should_be(2);
}
}
})

Expand Down Expand Up @@ -509,6 +543,29 @@ describe('Binding: Selected Options', {

value_of(selection()).should_be(["A", cObject]);
value_of(selection()[1] === cObject).should_be(true); // Also check with strict equality, because we don't want to falsely accept [object Object] == cObject
},

'Should update the model when selection in the SELECT node inside an optgroup changes': function () {
function setMultiSelectOptionSelectionState(optionElement, state) {
// Workaround an IE 6 bug (http://benhollis.net/experiments/browserdemos/ie6-adding-options.html)
if (/MSIE 6/i.test(navigator.userAgent))
optionElement.setAttribute('selected', state);
else
optionElement.selected = state;
}

var selection = new ko.observableArray([]);
testNode.innerHTML = "<select multiple='multiple' data-bind='selectedOptions:mySelection'><optgroup label='group'><option value='a'>a-text</option><option value='b'>b-text</option><option value='c'>c-text</option></optgroup></select>";
ko.applyBindings({ mySelection: selection }, testNode);

value_of(selection()).should_be([]);

setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[0], true);
setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[1], false);
setMultiSelectOptionSelectionState(testNode.childNodes[0].childNodes[0].childNodes[2], true);
ko.utils.triggerEvent(testNode.childNodes[0], "change");

value_of(selection()).should_be(['a', 'c']);
}
});

Expand Down Expand Up @@ -655,6 +712,17 @@ describe('Binding: CSS class name', {
observable2(false);
ko.processAllDeferredBindingUpdates();
value_of(testNode.childNodes[0].className).should_be("unrelatedClass1 unrelatedClass2 myRule");
},

'Should give the element a single CSS class without a leading space when the specified value is true': function() {
var observable1 = new ko.observable();
testNode.innerHTML = "<div data-bind='css: { myRule: someModelProperty }'>Hallo</div>";
ko.applyBindings({ someModelProperty: observable1 }, testNode);

value_of(testNode.childNodes[0].className).should_be("");
observable1(true);
ko.processAllDeferredBindingUpdates();
value_of(testNode.childNodes[0].className).should_be("myRule");
}
});

Expand Down Expand Up @@ -909,6 +977,19 @@ describe('Binding: Attr', {
ko.processAllDeferredBindingUpdates();
value_of(testNode.childNodes[0].getAttribute("someAttrib")).should_be(null);
});
},

'Should be able to set class attribute and access it using className property': function() {
var model = { myprop : ko.observable("newClass") };
testNode.innerHTML = "<div class='oldClass' data-bind=\"attr: {'class': myprop}\"></div>";
value_of(testNode.childNodes[0].className).should_be("oldClass");
ko.applyBindings(model, testNode);
value_of(testNode.childNodes[0].className).should_be("newClass");
// Should be able to clear class also
model.myprop(undefined);
ko.processAllDeferredBindingUpdates();
value_of(testNode.childNodes[0].className).should_be("");
value_of(testNode.childNodes[0].getAttribute("class")).should_be(null);
}
});

Expand Down Expand Up @@ -1290,6 +1371,26 @@ describe('Binding: Foreach', {
value_of(testNode.childNodes[0]).should_contain_html('<span data-bind="text: childprop">first child</span><span data-bind="text: childprop">second child</span>');
},

'Should clean away any data values attached to the original template nodes before use': function() {
// Represents issue https://github.com/SteveSanderson/knockout/pull/420
testNode.innerHTML = "<div data-bind='foreach: [1, 2]'><span></span></div>";

// Apply some DOM Data to the SPAN
var span = testNode.childNodes[0].childNodes[0];
value_of(span.tagName).should_be("SPAN");
ko.utils.domData.set(span, "mydata", 123);

// See that it vanishes because the SPAN is extracted as a template
value_of(ko.utils.domData.get(span, "mydata")).should_be(123);
ko.applyBindings(null, testNode);
value_of(ko.utils.domData.get(span, "mydata")).should_be(undefined);

// Also be sure the DOM Data doesn't appear in the output
value_of(testNode.childNodes[0]).should_contain_html('<span></span><span></span>');
value_of(ko.utils.domData.get(testNode.childNodes[0].childNodes[0], "mydata")).should_be(undefined);
value_of(ko.utils.domData.get(testNode.childNodes[0].childNodes[1], "mydata")).should_be(undefined);
},

'Should be able to use $data to reference each array item being bound': function() {
testNode.innerHTML = "<div data-bind='foreach: someItems'><span data-bind='text: $data'></span></div>";
var someItems = ['alpha', 'beta'];
Expand Down Expand Up @@ -1590,11 +1691,11 @@ describe('Binding: Foreach', {

'Should be able to output HTML5 elements within container-less templates (same as above)': function() {
// Represents https://github.com/SteveSanderson/knockout/issues/194
ko.utils.setHtml(testNode, "<!-- ko foreach:someitems --><div><section data-bind='text: $data'></section></div><!-- /ko -->");
ko.utils.setHtml(testNode, "xxx<!-- ko foreach:someitems --><div><section data-bind='text: $data'></section></div><!-- /ko -->");
var viewModel = {
someitems: [ 'Alpha', 'Beta' ]
};
ko.applyBindings(viewModel, testNode);
value_of(testNode).should_contain_html('<!-- ko foreach:someitems --><div><section data-bind="text: $data">alpha</section></div><div><section data-bind="text: $data">beta</section></div><!-- /ko -->');
value_of(testNode).should_contain_html('xxx<!-- ko foreach:someitems --><div><section data-bind="text: $data">alpha</section></div><div><section data-bind="text: $data">beta</section></div><!-- /ko -->');
}
});
});
24 changes: 19 additions & 5 deletions spec/dependentObservableBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ describe('Dependent Observable', {
value_of(ko.isObservable(instance)).should_be(true);
},

'Should advertise that instances are computed': function () {
var instance = new ko.dependentObservable(function () { });
value_of(ko.isComputed(instance)).should_be(true);
},

'Should advertise that instances cannot have values written to them': function () {
var instance = new ko.dependentObservable(function () { });
value_of(ko.isWriteableObservable(instance)).should_be(false);
Expand Down Expand Up @@ -158,7 +163,6 @@ describe('Dependent Observable', {
var notifiedValue;
var observable = new ko.observable(1);
var depedentObservable = new ko.dependentObservable(function () { return observable() + 1; });
depedentObservable.deferUpdates = false;
depedentObservable.subscribe(function (value) { notifiedValue = value; });

ko.processAllDeferredUpdates();
Expand All @@ -172,11 +176,11 @@ describe('Dependent Observable', {
var notifiedValue;
var observable = new ko.observable(1);
var depedentObservable = new ko.dependentObservable(function () { return observable() + 1; });
depedentObservable.deferUpdates = false;
depedentObservable.subscribe(function (value) { notifiedValue = value; }, null, "beforeChange");

value_of(notifiedValue).should_be(undefined);
observable(2);
ko.processAllDeferredUpdates();
value_of(notifiedValue).should_be(2);
value_of(depedentObservable()).should_be(3);
},
Expand All @@ -185,7 +189,6 @@ describe('Dependent Observable', {
var notifiedValues = [];
var observable = new ko.observable();
var depedentObservable = new ko.dependentObservable(function () { return observable() * observable(); });
depedentObservable.deferUpdates = false;
depedentObservable.subscribe(function (value) { notifiedValues.push(value); });
observable(2);
ko.processAllDeferredUpdates();
Expand Down Expand Up @@ -222,12 +225,12 @@ describe('Dependent Observable', {
null,
{ disposeWhen: function () { return timeToDispose; } }
);
dependent.deferUpdates = false;
value_of(timesEvaluated).should_be(1);
value_of(dependent.getDependenciesCount()).should_be(1);

timeToDispose = true;
underlyingObservable(101);
ko.processAllDeferredUpdates();
value_of(timesEvaluated).should_be(1);
value_of(dependent.getDependenciesCount()).should_be(0);
},
Expand All @@ -249,5 +252,16 @@ describe('Dependent Observable', {
value_of(timesEvaluated).should_be(0);
value_of(instance()).should_be(123);
value_of(timesEvaluated).should_be(1);
},

'Should prevent recursive calling of read function': function() {
var observable = ko.observable(0),
computed = ko.dependentObservable(function() {
// this both reads and writes to the observable
// will result in errors like "Maximum call stack size exceeded" (chrome)
// or "Out of stack space" (IE) or "too much recursion" (Firefox) if recursion
// isn't prevented
observable(observable() + 1);
});
}
})
})
2 changes: 1 addition & 1 deletion spec/domNodeDisposalBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ describe('DOM node disposal', {
ko.cleanNode(testNode);
value_of(didRun).should_be(false); // Didn't run only because we removed it
}
});
});
Loading

0 comments on commit e493625

Please sign in to comment.