Skip to content

Commit

Permalink
Use Jasmine's 'after' and custom 'restoreAfter' methods to clean up a…
Browse files Browse the repository at this point in the history
…fter tests that modify Knockout internals.
  • Loading branch information
mbest committed Aug 28, 2013
1 parent 73f2f15 commit 5cf5e95
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 142 deletions.
26 changes: 12 additions & 14 deletions spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ describe('Binding attribute syntax', function() {
beforeEach(jasmine.prepareTestNode);

it('applyBindings should accept no parameters and then act on document.body with undefined model', function() {
this.after(function () { ko.utils.domData.clear(document.body); }); // Just to avoid interfering with other specs

var didInit = false;
ko.bindingHandlers.test = {
init: function (element, valueAccessor, allBindings, viewModel) {
Expand All @@ -13,12 +15,11 @@ describe('Binding attribute syntax', function() {
testNode.innerHTML = "<div id='testElement' data-bind='test:123'></div>";
ko.applyBindings();
expect(didInit).toEqual(true);

// Just to avoid interfering with other specs:
ko.utils.domData.clear(document.body);
});

it('applyBindings should accept one parameter and then act on document.body with parameter as model', function() {
this.after(function () { ko.utils.domData.clear(document.body); }); // Just to avoid interfering with other specs

var didInit = false;
var suppliedViewModel = {};
ko.bindingHandlers.test = {
Expand All @@ -31,9 +32,6 @@ describe('Binding attribute syntax', function() {
testNode.innerHTML = "<div id='testElement' data-bind='test:123'></div>";
ko.applyBindings(suppliedViewModel);
expect(didInit).toEqual(true);

// Just to avoid interfering with other specs:
ko.utils.domData.clear(document.body);
});

it('applyBindings should accept two parameters and then act on second param as DOM node with first param as model', function() {
Expand All @@ -47,15 +45,14 @@ describe('Binding attribute syntax', function() {
}
};
testNode.innerHTML = "<div id='testElement' data-bind='test:123'></div>";

var shouldNotMatchNode = document.createElement("DIV");
shouldNotMatchNode.innerHTML = "<div id='shouldNotMatchThisElement' data-bind='test:123'></div>";
document.body.appendChild(shouldNotMatchNode);
try {
ko.applyBindings(suppliedViewModel, testNode);
expect(didInit).toEqual(true);
} finally {
shouldNotMatchNode.parentNode.removeChild(shouldNotMatchNode);
}
this.after(function () { document.body.removeChild(shouldNotMatchNode); });

ko.applyBindings(suppliedViewModel, testNode);
expect(didInit).toEqual(true);
});

it('Should tolerate empty or only white-space binding strings', function() {
Expand Down Expand Up @@ -408,13 +405,16 @@ describe('Binding attribute syntax', function() {
});

it('Should not bind against text content inside <script> tags', function() {
this.restoreAfter(ko.bindingProvider, 'instance');

// Developers won't expect or want binding to mutate the contents of <script> tags.
// Historically this wasn't a problem because the default binding provider only acts
// on elements, but now custom providers can act on text contents of elements, it's
// important to ensure we don't break <script> elements by mutating their contents.

// First replace the binding provider with one that's hardcoded to replace all text
// content with a special message, via a binding handler that operates on text nodes

var originalBindingProvider = ko.bindingProvider.instance;
ko.bindingProvider.instance = {
nodeHasBindings: function(node, bindingContext) {
Expand All @@ -439,7 +439,5 @@ describe('Binding attribute syntax', function() {
testNode.innerHTML = "<p>Hello</p><script>alert(123);</script><p>Goodbye</p>";
ko.applyBindings({ sometext: 'hello' }, testNode);
expect(testNode).toContainHtml('<p>replaced</p><script>alert(123);</script><p>replaced</p>');

ko.bindingProvider.instance = originalBindingProvider;
});
});
4 changes: 2 additions & 2 deletions spec/bindingPreprocessingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ describe('Binding preprocessing', function() {
});

it('Should be able to get a dynamically created binding handler during preprocessing', function() {
var oldGetHandler = ko.getBindingHandler;
this.restoreAfter(ko, 'getBindingHandler'); // restore original function when done

ko.getBindingHandler = function(bindingKey) {
return {
preprocess: function(value) {
Expand All @@ -66,7 +67,6 @@ describe('Binding preprocessing', function() {
};
};
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1");
ko.getBindingHandler = oldGetHandler; // restore original function

var parsedRewritten = eval("({" + rewritten + "})");
expect(parsedRewritten.a).toEqual(12);
Expand Down
4 changes: 2 additions & 2 deletions spec/defaultBindings/textBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ describe('Binding: Text', function() {
});

it('Should not attempt data binding on the generated text node', function() {
this.restoreAfter(ko.bindingProvider, 'instance');

// Since custom binding providers can regard text nodes as bindable, it would be a
// security risk to bind against user-supplied text (XSS).

Expand Down Expand Up @@ -77,7 +79,5 @@ describe('Binding: Text', function() {
testNode.innerHTML = "<span data-bind='text: sometext'></span>";
ko.applyBindings({ sometext: 'hello' }, testNode);
expect("textContent" in testNode ? testNode.textContent : testNode.innerText).toEqual('hello');

ko.bindingProvider.instance = originalBindingProvider;
});
});
7 changes: 7 additions & 0 deletions spec/lib/jasmine.extensions.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
jasmine.Spec.prototype.restoreAfter = function(object, propertyName) {
var originalValue = object[propertyName];
this.after(function() {
object[propertyName] = originalValue;
});
};

jasmine.Matchers.prototype.toEqualOneOf = function (expectedPossibilities) {
for (var i = 0; i < expectedPossibilities.length; i++) {
if (this.env.equals_(this.actual, expectedPossibilities[i])) {
Expand Down
189 changes: 83 additions & 106 deletions spec/nodePreprocessingBehaviors.js
Original file line number Diff line number Diff line change
@@ -1,129 +1,106 @@
describe('Node preprocessing', function() {
beforeEach(jasmine.prepareTestNode);

function withPreprocessor(options) {
var originalBindingProvider = ko.bindingProvider.instance,
preprocessingBindingProvider = function() { };
preprocessingBindingProvider.prototype = originalBindingProvider;
ko.bindingProvider.instance = new preprocessingBindingProvider();
ko.bindingProvider.instance.preprocessNode = options.preprocessNode;
beforeEach(function() {
this.restoreAfter(ko.bindingProvider, 'instance');

try {
options.run();
} finally {
ko.bindingProvider.instance = originalBindingProvider;
}
}
var preprocessingBindingProvider = function() { };
preprocessingBindingProvider.prototype = ko.bindingProvider.instance;
ko.bindingProvider.instance = new preprocessingBindingProvider();
});

it('Can leave the nodes unchanged by returning a falsey value', function() {
withPreprocessor({
preprocessNode: function(node) { return null; },
run: function() {
testNode.innerHTML = "<p data-bind='text: someValue'></p>";
ko.applyBindings({ someValue: 'hello' }, testNode);
expect(testNode).toContainText('hello');
}
});
ko.bindingProvider.instance.preprocessNode = function(node) { return null; };
testNode.innerHTML = "<p data-bind='text: someValue'></p>";
ko.applyBindings({ someValue: 'hello' }, testNode);
expect(testNode).toContainText('hello');
});

it('Can replace a node with some other node', function() {
withPreprocessor({
preprocessNode: function(node) {
// Example: replace <mySpecialNode /> with <span data-bind='text: someValue'></span>
// This technique could be the basis for implementing custom element types that render templates
if (node.tagName && node.tagName.toLowerCase() === 'myspecialnode') {
var newNode = document.createElement("span");
newNode.setAttribute("data-bind", "text: someValue");
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
return [newNode];
}
},
run: function() {
testNode.innerHTML = "<p>a</p><mySpecialNode></mySpecialNode><p>b</p>";
var someValue = ko.observable('hello');
ko.applyBindings({ someValue: someValue }, testNode);
expect(testNode).toContainText('ahellob');

// Check that updating the observable has the expected effect
someValue('goodbye');
expect(testNode).toContainText('agoodbyeb');
ko.bindingProvider.instance.preprocessNode = function(node) {
// Example: replace <mySpecialNode /> with <span data-bind='text: someValue'></span>
// This technique could be the basis for implementing custom element types that render templates
if (node.tagName && node.tagName.toLowerCase() === 'myspecialnode') {
var newNode = document.createElement("span");
newNode.setAttribute("data-bind", "text: someValue");
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
return [newNode];
}
});
};
testNode.innerHTML = "<p>a</p><mySpecialNode></mySpecialNode><p>b</p>";
var someValue = ko.observable('hello');
ko.applyBindings({ someValue: someValue }, testNode);
expect(testNode).toContainText('ahellob');

// Check that updating the observable has the expected effect
someValue('goodbye');
expect(testNode).toContainText('agoodbyeb');
});

it('Can replace a node with multiple new nodes', function() {
withPreprocessor({
preprocessNode: function(node) {
// Example: Replace {{ someValue }} with text from that property.
// This could be generalized to full support for string interpolation in text nodes.
if (node.nodeType === 3 && node.data.indexOf("{{ someValue }}") >= 0) {
var prefix = node.data.substring(0, node.data.indexOf("{{ someValue }}")),
suffix = node.data.substring(node.data.indexOf("{{ someValue }}") + "{{ someValue }}".length),
newNodes = [
document.createTextNode(prefix),
document.createComment("ko text: someValue"),
document.createComment("/ko"),
document.createTextNode(suffix)
];
// Manually reimplement ko.utils.replaceDomNodes, since it's not available in minified build
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
node.parentNode.removeChild(node);
return newNodes;
ko.bindingProvider.instance.preprocessNode = function(node) {
// Example: Replace {{ someValue }} with text from that property.
// This could be generalized to full support for string interpolation in text nodes.
if (node.nodeType === 3 && node.data.indexOf("{{ someValue }}") >= 0) {
var prefix = node.data.substring(0, node.data.indexOf("{{ someValue }}")),
suffix = node.data.substring(node.data.indexOf("{{ someValue }}") + "{{ someValue }}".length),
newNodes = [
document.createTextNode(prefix),
document.createComment("ko text: someValue"),
document.createComment("/ko"),
document.createTextNode(suffix)
];
// Manually reimplement ko.utils.replaceDomNodes, since it's not available in minified build
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
},
run: function() {
testNode.innerHTML = "the value is {{ someValue }}.";
var someValue = ko.observable('hello');
ko.applyBindings({ someValue: someValue }, testNode);
expect(testNode).toContainText('the value is hello.');

// Check that updating the observable has the expected effect
someValue('goodbye');
expect(testNode).toContainText('the value is goodbye.');
node.parentNode.removeChild(node);
return newNodes;
}
});
};
testNode.innerHTML = "the value is {{ someValue }}.";
var someValue = ko.observable('hello');
ko.applyBindings({ someValue: someValue }, testNode);
expect(testNode).toContainText('the value is hello.');

// Check that updating the observable has the expected effect
someValue('goodbye');
expect(testNode).toContainText('the value is goodbye.');
});

it('Can modify the set of top-level nodes in a foreach loop', function() {
withPreprocessor({
preprocessNode: function(node) {
// Replace <data /> with <span data-bind="text: $data"></span>
if (node.tagName && node.tagName.toLowerCase() === "data") {
var newNode = document.createElement("span");
newNode.setAttribute("data-bind", "text: $data");
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
return [newNode];
}

// Delete any <button> elements
if (node.tagName && node.tagName.toLowerCase() === "button") {
node.parentNode.removeChild(node);
return [];
}
},
ko.bindingProvider.instance.preprocessNode = function(node) {
// Replace <data /> with <span data-bind="text: $data"></span>
if (node.tagName && node.tagName.toLowerCase() === "data") {
var newNode = document.createElement("span");
newNode.setAttribute("data-bind", "text: $data");
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
return [newNode];
}

run: function() {
testNode.innerHTML = "<div data-bind='foreach: items'>"
+ "<button>DeleteMe</button>"
+ "<data></data>"
+ "<!-- ko text: $data --><!-- /ko -->"
+ "<button>DeleteMe</button>" // Tests that we can remove the last node even when the preceding node is a virtual element rather than a single node
+ "</div>";
var items = ko.observableArray(["Alpha", "Beta"]);
// Delete any <button> elements
if (node.tagName && node.tagName.toLowerCase() === "button") {
node.parentNode.removeChild(node);
return [];
}
};
testNode.innerHTML = "<div data-bind='foreach: items'>"
+ "<button>DeleteMe</button>"
+ "<data></data>"
+ "<!-- ko text: $data --><!-- /ko -->"
+ "<button>DeleteMe</button>" // Tests that we can remove the last node even when the preceding node is a virtual element rather than a single node
+ "</div>";
var items = ko.observableArray(["Alpha", "Beta"]);

ko.applyBindings({ items: items }, testNode);
expect(testNode).toContainText('AlphaAlphaBetaBeta');
ko.applyBindings({ items: items }, testNode);
expect(testNode).toContainText('AlphaAlphaBetaBeta');

// Check that modifying the observable array has the expected effect
items.splice(0, 1);
expect(testNode).toContainText('BetaBeta');
items.push('Gamma');
expect(testNode).toContainText('BetaBetaGammaGamma');
}
});
// Check that modifying the observable array has the expected effect
items.splice(0, 1);
expect(testNode).toContainText('BetaBeta');
items.push('Gamma');
expect(testNode).toContainText('BetaBetaGammaGamma');
});
});
Loading

0 comments on commit 5cf5e95

Please sign in to comment.