Skip to content

Commit

Permalink
Clean up async tests, "extensions", and runner code for Jasmine.
Browse files Browse the repository at this point in the history
  • Loading branch information
mbest committed Nov 17, 2012
1 parent c236572 commit d63b4ba
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 181 deletions.
195 changes: 92 additions & 103 deletions spec/asyncBehaviors.js
@@ -1,116 +1,105 @@
describe("Throttled observables", function() {

it("Should notify subscribers asynchronously after writes stop for the specified timeout duration", function() {
var observable = ko.observable('A').extend({ throttle: 50 });
var notifiedValues = [];
observable.subscribe(function(value) {
notifiedValues.push(value);
it("Should notify subscribers asynchronously after writes stop for the specified timeout duration", function() {
var observable = ko.observable('A').extend({ throttle: 50 });
var notifiedValues = [];
observable.subscribe(function(value) {
notifiedValues.push(value);
});

// Mutate a few times
observable('B');
observable('C');
observable('D');
expect(notifiedValues.length).toEqual(0); // Should not notify synchronously

// Wait
waits(20);
runs(function() {
// Mutate more
observable('E');
observable('F');
expect(notifiedValues.length).toEqual(0); // Should not notify until end of throttle timeout
});

// Wait until after timeout
waitsFor(function() {
return notifiedValues.length > 0;
}, 60);
runs(function() {
expect(notifiedValues.length).toEqual(1);
expect(notifiedValues[0]).toEqual("F");
});
});

runs(function() {

// Mutate a few times
observable('B');
observable('C');
observable('D');
expect(notifiedValues.length).toEqual(0); // Should not notify synchronously

// Wait
setTimeout(function() {
// Mutate more
observable('E');
observable('F');
expect(notifiedValues.length).toEqual(0); // Should not notify until end of throttle timeout
}, 20);
});

waitsFor(function() {
// Wait until after timeout
return notifiedValues.length > 0;
}, 80);

runs(function() {
expect(notifiedValues.length).toEqual(1);
expect(notifiedValues[0]).toEqual("F");
});

});
});

describe("Throttled dependent observables", function() {

it("Should notify subscribers asynchronously after dependencies stop updating for the specified timeout duration", function() {
var underlying = ko.observable();
var asyncDepObs = ko.dependentObservable(function() {
return underlying();
}).extend({ throttle: 100 });
var notifiedValues = [];
asyncDepObs.subscribe(function(value) {
notifiedValues.push(value);
});


runs(function() {
// Check initial state
expect(asyncDepObs()).toBeUndefined();

// Mutate
underlying('New value');
expect(asyncDepObs()).toBeUndefined(); // Should not update synchronously
expect(notifiedValues.length).toEqual(0);

// Wait
setTimeout(function() {
// After 50ms, still shouldn't have evaluated
expect(asyncDepObs()).toBeUndefined(); // Should not update until throttle timeout
it("Should notify subscribers asynchronously after dependencies stop updating for the specified timeout duration", function() {
var underlying = ko.observable();
var asyncDepObs = ko.dependentObservable(function() {
return underlying();
}).extend({ throttle: 100 });
var notifiedValues = [];
asyncDepObs.subscribe(function(value) {
notifiedValues.push(value);
});

// Check initial state
expect(asyncDepObs()).toBeUndefined();

// Mutate
underlying('New value');
expect(asyncDepObs()).toBeUndefined(); // Should not update synchronously
expect(notifiedValues.length).toEqual(0);
}, 50);
});

waitsFor(function() {
// Now wait for throttle timeout
return notifiedValues.length > 0;
}, 110);

runs(function() {
expect(asyncDepObs()).toEqual('New value');
expect(notifiedValues.length).toEqual(1);
expect(notifiedValues[0]).toEqual('New value');
});

});

it("Should run evaluator only once when dependencies stop updating for the specified timeout duration", function() {
var evaluationCount = 0;
var someDependency = ko.observable();
var asyncDepObs = ko.dependentObservable(function() {
evaluationCount++;
return someDependency();
}).extend({ throttle: 100 });

runs(function() {
// Mutate a few times synchronously
expect(evaluationCount).toEqual(1); // Evaluates synchronously when first created, like all dependent observables
someDependency("A");
someDependency("B");
someDependency("C");
expect(evaluationCount).toEqual(1); // Should not re-evaluate synchronously when dependencies update

// Also mutate async
setTimeout(function() {
someDependency("D");
expect(evaluationCount).toEqual(1);
}, 10);
// After 50ms, still shouldn't have evaluated
waits(50);
runs(function() {
expect(asyncDepObs()).toBeUndefined(); // Should not update until throttle timeout
expect(notifiedValues.length).toEqual(0);
});

// Now wait for throttle timeout
waitsFor(function() {
return notifiedValues.length > 0;
}, 60);
runs(function() {
expect(asyncDepObs()).toEqual('New value');
expect(notifiedValues.length).toEqual(1);
expect(notifiedValues[0]).toEqual('New value');
});
});

waitsFor(function() {
// Now wait for throttle timeout
return evaluationCount > 1;
}, 120);

runs(function() {
expect(evaluationCount).toEqual(2); // Finally, it's evaluated
expect(asyncDepObs()).toEqual("D");
it("Should run evaluator only once when dependencies stop updating for the specified timeout duration", function() {
var evaluationCount = 0;
var someDependency = ko.observable();
var asyncDepObs = ko.dependentObservable(function() {
evaluationCount++;
return someDependency();
}).extend({ throttle: 100 });

// Mutate a few times synchronously
expect(evaluationCount).toEqual(1); // Evaluates synchronously when first created, like all dependent observables
someDependency("A");
someDependency("B");
someDependency("C");
expect(evaluationCount).toEqual(1); // Should not re-evaluate synchronously when dependencies update

// Also mutate async
waits(10);
runs(function() {
someDependency("D");
expect(evaluationCount).toEqual(1);
});

// Now wait for throttle timeout
waitsFor(function() {
return evaluationCount > 1;
}, 110);
runs(function() {
expect(evaluationCount).toEqual(2); // Finally, it's evaluated
expect(asyncDepObs()).toEqual("D");
});
});
});
});
117 changes: 57 additions & 60 deletions spec/lib/jasmine.extensions.js
@@ -1,77 +1,74 @@
beforeEach(function() {
this.addMatchers({
toEqualOneOf: function (expectedPossibilities) {
for (var i = 0; i < expectedPossibilities.length; i++) {
jasmine.Matchers.prototype.toEqualOneOf = function (expectedPossibilities) {
for (var i = 0; i < expectedPossibilities.length; i++) {
if (this.env.equals_(this.actual, expectedPossibilities[i])) {
return true;
return true;
}
}
return false;
},
toContainHtml: function (expectedHtml) {
var cleanedHtml = this.actual.innerHTML.toLowerCase().replace(/\r\n/g, "");
// IE < 9 strips whitespace immediately following comment nodes. Normalize by doing the same on all browsers.
cleanedHtml = cleanedHtml.replace(/(<!--.*?-->)\s*/g, "$1");
expectedHtml = expectedHtml.replace(/(<!--.*?-->)\s*/g, "$1");
// Also remove __ko__ expando properties (for DOM data) - most browsers hide these anyway but IE < 9 includes them in innerHTML
cleanedHtml = cleanedHtml.replace(/ __ko__\d+=\"(ko\d+|null)\"/g, "");
// Fix explanatory message
this.actual = cleanedHtml;
return cleanedHtml === expectedHtml;
},
toContainText: function (expectedText) {
var actualText = 'textContent' in this.actual ? this.actual.textContent : this.actual.innerText;
var cleanedActualText = actualText.replace(/\r\n/g, "\n");
// Fix explanatory message
this.actual = cleanedActualText;
return cleanedActualText === expectedText;
},
toHaveOwnProperties: function (expectedProperties) {
var ownProperties = [];
for (var prop in this.actual) {
}
return false;
};

jasmine.Matchers.prototype.toContainHtml = function (expectedHtml) {
var cleanedHtml = this.actual.innerHTML.toLowerCase().replace(/\r\n/g, "");
// IE < 9 strips whitespace immediately following comment nodes. Normalize by doing the same on all browsers.
cleanedHtml = cleanedHtml.replace(/(<!--.*?-->)\s*/g, "$1");
expectedHtml = expectedHtml.replace(/(<!--.*?-->)\s*/g, "$1");
// Also remove __ko__ expando properties (for DOM data) - most browsers hide these anyway but IE < 9 includes them in innerHTML
cleanedHtml = cleanedHtml.replace(/ __ko__\d+=\"(ko\d+|null)\"/g, "");
this.actual = cleanedHtml; // Fix explanatory message
return cleanedHtml === expectedHtml;
};

jasmine.Matchers.prototype.toContainText = function (expectedText) {
var actualText = 'textContent' in this.actual ? this.actual.textContent : this.actual.innerText;
var cleanedActualText = actualText.replace(/\r\n/g, "\n");
this.actual = cleanedActualText; // Fix explanatory message
return cleanedActualText === expectedText;
};

jasmine.Matchers.prototype.toHaveOwnProperties = function (expectedProperties) {
var ownProperties = [];
for (var prop in this.actual) {
if (this.actual.hasOwnProperty(prop)) {
ownProperties.push(prop);
ownProperties.push(prop);
}
}
return this.env.equals_(ownProperties, expectedProperties);
},
toHaveSelectedValues: function (expectedValues) {
var selectedNodes = ko.utils.arrayFilter(this.actual.childNodes, function (node) { return node.selected; }),
selectedValues = ko.utils.arrayMap(selectedNodes, function (node) { return ko.selectExtensions.readValue(node); });
// Fix explanatory message
this.actual = selectedValues;
return this.env.equals_(selectedValues, expectedValues);
}
});
});
return this.env.equals_(ownProperties, expectedProperties);
};

jasmine.Matchers.prototype.toHaveSelectedValues = function (expectedValues) {
var selectedNodes = ko.utils.arrayFilter(this.actual.childNodes, function (node) { return node.selected; }),
selectedValues = ko.utils.arrayMap(selectedNodes, function (node) { return ko.selectExtensions.readValue(node); });
this.actual = selectedValues; // Fix explanatory message
return this.env.equals_(selectedValues, expectedValues);
};

jasmine.addScriptReference = function(scriptUrl) {
if (window.console)
console.log("Loading " + scriptUrl + "...");
document.write("<scr" + "ipt type='text/javascript' src='" + scriptUrl + "'></sc" + "ript>");
if (window.console)
console.log("Loading " + scriptUrl + "...");
document.write("<scr" + "ipt type='text/javascript' src='" + scriptUrl + "'></sc" + "ript>");
};

jasmine.prepareTestNode = function() {
// The bindings specs make frequent use of this utility function to set up
// a clean new DOM node they can execute code against
var existingNode = document.getElementById("testNode");
if (existingNode != null)
existingNode.parentNode.removeChild(existingNode);
testNode = document.createElement("div");
testNode.id = "testNode";
document.body.appendChild(testNode);
// The bindings specs make frequent use of this utility function to set up
// a clean new DOM node they can execute code against
var existingNode = document.getElementById("testNode");
if (existingNode != null)
existingNode.parentNode.removeChild(existingNode);
testNode = document.createElement("div");
testNode.id = "testNode";
document.body.appendChild(testNode);
};

// Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
// Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
// If there is a future need to detect specific versions of IE10+, we will amend this.
jasmine.ieVersion = (function() {
var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');

// Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
while (
div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
iElems[0]
);
return version > 4 ? version : undefined;
}());
// Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
while (
div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
iElems[0]
);
return version > 4 ? version : undefined;
}());
22 changes: 4 additions & 18 deletions spec/runner.html
Expand Up @@ -14,6 +14,7 @@
<script type="text/javascript" src="lib/jasmine-1.2.0/jasmine-html.js"></script>

<!-- our jasmine extensions -->
<link rel="stylesheet" type="text/css" href="lib/jasmine.extensions.css" />

This comment has been minimized.

Copy link
@SteveSanderson

SteveSanderson Nov 20, 2012

Contributor

Did you mean to add this file? It doesn't appear to be in the repo.

This comment has been minimized.

Copy link
@mbest

mbest Nov 20, 2012

Author Member

I've added the file now.

<script type="text/javascript" src="lib/jasmine.extensions.js"></script>

<!-- knockout polyfills -->
Expand Down Expand Up @@ -69,28 +70,13 @@
<script type="text/javascript">
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
jasmineEnv.updateInterval = 100;

var htmlReporter = new jasmine.HtmlReporter();

jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = htmlReporter.specFilter;

jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};

var currentWindowOnload = window.onload;

window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};

function execJasmine() {
jasmineEnv.execute();
}
jasmineEnv.execute();

})();
</script>
Expand Down

0 comments on commit d63b4ba

Please sign in to comment.