Skip to content

Commit

Permalink
Merge branch '751-node-support'
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveSanderson committed Feb 26, 2013
2 parents b9598ee + 51758a6 commit 5af83af
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 106 deletions.
3 changes: 3 additions & 0 deletions build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ rm -f output/*.js~
# Run tests in Phantomjs if available
command -v phantomjs >/dev/null && (cd ..; echo; phantomjs spec/runner.phantom.js || handle_fail)

# Run tests in Nodejs if available
command -v node >/dev/null && (cd ..; echo; node spec/runner.node.js || handle_fail)

echo; echo "Build succeeded"
2 changes: 1 addition & 1 deletion build/fragments/extern-post.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
})(window,document,navigator,window["jQuery"]);
}());
7 changes: 6 additions & 1 deletion build/fragments/extern-pre.js
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
(function(window,document,navigator,jQuery,undefined){
(function(undefined){
var window = this || (0,eval)('this'), // Use global 'this'
document = window['document'],
navigator = window['navigator'],
jQuery = window["jQuery"],
JSON = window["JSON"];
75 changes: 75 additions & 0 deletions spec/arrayEditDetectionBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

describe('Compare Arrays', function() {
it('Should recognize when two arrays have the same contents', function () {
var subject = ["A", {}, function () { } ];
var compareResult = ko.utils.compareArrays(subject, subject.slice(0));

expect(compareResult.length).toEqual(subject.length);
for (var i = 0; i < subject.length; i++) {
expect(compareResult[i].status).toEqual("retained");
expect(compareResult[i].value).toEqual(subject[i]);
}
});

it('Should recognize added items', function () {
var oldArray = ["A", "B"];
var newArray = ["A", "A2", "A3", "B", "B2"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "retained", value: "A" },
{ status: "added", value: "A2", index: 1 },
{ status: "added", value: "A3", index: 2 },
{ status: "retained", value: "B" },
{ status: "added", value: "B2", index: 4 }
]);
});

it('Should recognize deleted items', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = ["B", "C", "E"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "deleted", value: "A", index: 0 },
{ status: "retained", value: "B" },
{ status: "retained", value: "C" },
{ status: "deleted", value: "D", index: 3 },
{ status: "retained", value: "E" }
]);
});

it('Should recognize mixed edits', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = [123, "A", "E", "C", "D"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "added", value: 123, index: 0 },
{ status: "retained", value: "A" },
{ status: "deleted", value: "B", index: 1 },
{ status: "added", value: "E", index: 2, moved: 4 },
{ status: "retained", value: "C" },
{ status: "retained", value: "D" },
{ status: "deleted", value: "E", index: 4, moved: 2 }
]);
});

it('Should recognize replaced array', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = ["F", "G", "H", "I", "J"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
// The order of added and deleted doesn't really matter. We sort by a property that
// contains unique values to ensure the results are in a known order for verification.
compareResult.sort(function(a, b) { return a.value.localeCompare(b.value) });
expect(compareResult).toEqual([
{ status: "deleted", value: "A", index: 0},
{ status: "deleted", value: "B", index: 1},
{ status: "deleted", value: "C", index: 2},
{ status: "deleted", value: "D", index: 3},
{ status: "deleted", value: "E", index: 4},
{ status: "added", value: "F", index: 0},
{ status: "added", value: "G", index: 1},
{ status: "added", value: "H", index: 2},
{ status: "added", value: "I", index: 3},
{ status: "added", value: "J", index: 4}
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,81 +6,6 @@ function copyDomNodeChildren(domNode) {
return copy;
}

describe('Compare Arrays', function() {
it('Should recognize when two arrays have the same contents', function () {
var subject = ["A", {}, function () { } ];
var compareResult = ko.utils.compareArrays(subject, subject.slice(0));

expect(compareResult.length).toEqual(subject.length);
for (var i = 0; i < subject.length; i++) {
expect(compareResult[i].status).toEqual("retained");
expect(compareResult[i].value).toEqual(subject[i]);
}
});

it('Should recognize added items', function () {
var oldArray = ["A", "B"];
var newArray = ["A", "A2", "A3", "B", "B2"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "retained", value: "A" },
{ status: "added", value: "A2", index: 1 },
{ status: "added", value: "A3", index: 2 },
{ status: "retained", value: "B" },
{ status: "added", value: "B2", index: 4 }
]);
});

it('Should recognize deleted items', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = ["B", "C", "E"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "deleted", value: "A", index: 0 },
{ status: "retained", value: "B" },
{ status: "retained", value: "C" },
{ status: "deleted", value: "D", index: 3 },
{ status: "retained", value: "E" }
]);
});

it('Should recognize mixed edits', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = [123, "A", "E", "C", "D"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
expect(compareResult).toEqual([
{ status: "added", value: 123, index: 0 },
{ status: "retained", value: "A" },
{ status: "deleted", value: "B", index: 1 },
{ status: "added", value: "E", index: 2, moved: 4 },
{ status: "retained", value: "C" },
{ status: "retained", value: "D" },
{ status: "deleted", value: "E", index: 4, moved: 2 }
]);
});

it('Should recognize replaced array', function () {
var oldArray = ["A", "B", "C", "D", "E"];
var newArray = ["F", "G", "H", "I", "J"];
var compareResult = ko.utils.compareArrays(oldArray, newArray);
// The order of added and deleted doesn't really matter. We sort by a property that
// contains unique values to ensure the results are in a known order for verification.
compareResult.sort(function(a, b) { return a.value.localeCompare(b.value) });
expect(compareResult).toEqual([
{ status: "deleted", value: "A", index: 0},
{ status: "deleted", value: "B", index: 1},
{ status: "deleted", value: "C", index: 2},
{ status: "deleted", value: "D", index: 3},
{ status: "deleted", value: "E", index: 4},
{ status: "added", value: "F", index: 0},
{ status: "added", value: "G", index: 1},
{ status: "added", value: "H", index: 2},
{ status: "added", value: "I", index: 3},
{ status: "added", value: "J", index: 4}
]);
});
});

describe('Array to DOM node children mapping', function() {
beforeEach(jasmine.prepareTestNode);

Expand Down
21 changes: 1 addition & 20 deletions spec/dependentObservableBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Dependent Observable', function() {
var someContainer = { depObs: instance };
someContainer.depObs("some value");
expect(invokedWriteWithValue).toEqual("some value");
expect(invokedWriteWithThis).toEqual(window); // Since no owner was specified
expect(invokedWriteWithThis).toEqual(function(){return this;}.call()); // Since no owner was specified
});

it('Should be able to write to multiple computed properties on a model object using chaining syntax', function() {
Expand Down Expand Up @@ -270,25 +270,6 @@ describe('Dependent Observable', function() {
expect(dependentObservable.isActive()).toEqual(false);
});

it('Should register DOM node disposal callback only if active after the initial evaluation', function() {
// Set up an active one
var nodeForActive = document.createElement('DIV'),
observable = ko.observable('initial'),
activeDependentObservable = ko.dependentObservable({ read: function() { return observable(); }, disposeWhenNodeIsRemoved: nodeForActive });
var nodeForInactive = document.createElement('DIV')
inactiveDependentObservable = ko.dependentObservable({ read: function() { return 123; }, disposeWhenNodeIsRemoved: nodeForInactive });

expect(activeDependentObservable.isActive()).toEqual(true);
expect(inactiveDependentObservable.isActive()).toEqual(false);

// Infer existence of disposal callbacks from presence/absence of DOM data. This is really just an implementation detail,
// and so it's unusual to rely on it in a spec. However, the presence/absence of the callback isn't exposed in any other way,
// and if the implementation ever changes, this spec should automatically fail because we're checking for both the positive
// and negative cases.
expect(ko.utils.domData.clear(nodeForActive)).toEqual(true); // There was a callback
expect(ko.utils.domData.clear(nodeForInactive)).toEqual(false); // There was no callback
});

it('Should advertise that instances *can* have values written to them if you supply a "write" callback', function() {
var instance = new ko.dependentObservable({
read: function() {},
Expand Down
21 changes: 21 additions & 0 deletions spec/dependentObservableDomBehaviors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

describe('Dependent Observable DOM', function() {
it('Should register DOM node disposal callback only if active after the initial evaluation', function() {
// Set up an active one
var nodeForActive = document.createElement('DIV'),
observable = ko.observable('initial'),
activeDependentObservable = ko.dependentObservable({ read: function() { return observable(); }, disposeWhenNodeIsRemoved: nodeForActive });
var nodeForInactive = document.createElement('DIV')
inactiveDependentObservable = ko.dependentObservable({ read: function() { return 123; }, disposeWhenNodeIsRemoved: nodeForInactive });

expect(activeDependentObservable.isActive()).toEqual(true);
expect(inactiveDependentObservable.isActive()).toEqual(false);

// Infer existence of disposal callbacks from presence/absence of DOM data. This is really just an implementation detail,
// and so it's unusual to rely on it in a spec. However, the presence/absence of the callback isn't exposed in any other way,
// and if the implementation ever changes, this spec should automatically fail because we're checking for both the positive
// and negative cases.
expect(ko.utils.domData.clear(nodeForActive)).toEqual(true); // There was a callback
expect(ko.utils.domData.clear(nodeForInactive)).toEqual(false); // There was no callback
});
})
2 changes: 1 addition & 1 deletion spec/lib/jasmine.extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jasmine.prepareTestNode = function() {
// 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() {
jasmine.ieVersion = typeof(document) == 'undefined' ? undefined : (function() {
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
Expand Down
4 changes: 3 additions & 1 deletion spec/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@
</script>

<!-- specs -->
<script type="text/javascript" src="arrayEditDetectionBehaviors.js"></script>
<script type="text/javascript" src="arrayToDomEditDetectionBehaviors.js"></script>
<script type="text/javascript" src="asyncBehaviors.js"></script>
<script type="text/javascript" src="memoizationBehaviors.js"></script>
<script type="text/javascript" src="subscribableBehaviors.js"></script>
<script type="text/javascript" src="observableBehaviors.js"></script>
<script type="text/javascript" src="observableArrayBehaviors.js"></script>
<script type="text/javascript" src="dependentObservableBehaviors.js"></script>
<script type="text/javascript" src="dependentObservableDomBehaviors.js"></script>
<script type="text/javascript" src="extenderBehaviors.js"></script>
<script type="text/javascript" src="domNodeDisposalBehaviors.js"></script>
<script type="text/javascript" src="mappingHelperBehaviors.js"></script>
<script type="text/javascript" src="expressionRewritingBehaviors.js"></script>
<script type="text/javascript" src="bindingAttributeBehaviors.js"></script>
<script type="text/javascript" src="templatingBehaviors.js"></script>
<script type="text/javascript" src="editDetectionBehaviors.js"></script>
<script type="text/javascript" src="jsonPostingBehaviors.js"></script>
<script type="text/javascript" src="nativeTemplateEngineBehaviors.js"></script>

Expand Down
72 changes: 72 additions & 0 deletions spec/runner.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
console.log("Running Knockout tests in Node.js");

var fs = require('fs');
var jasmine = require('./lib/jasmine-1.2.0/jasmine');

// export jasmine globals
for (var key in jasmine) {
global[key] = jasmine[key];
}

// add our jasmine extensions to the exported globals
require('./lib/jasmine.extensions');

// export ko globals
if (process.argv.length > 2 && process.argv[2] == '--source') {
// equivalent of ../build/knockout-raw.js
global.DEBUG = true;
global.ko = global.koExports = {};
global.knockoutDebugCallback = function(sources) {
sources.unshift('build/fragments/extern-pre.js');
sources.push('build/fragments/extern-post.js');
eval(sources.reduce(function(all, source) {
return all + '\n' + fs.readFileSync(source);
}, ''));
};
require('../build/fragments/source-references');
} else {
global.ko = require('../build/output/knockout-latest.js');
}

// reference behaviors that should work out of browser
require('./arrayEditDetectionBehaviors');
require('./asyncBehaviors');
require('./dependentObservableBehaviors');
require('./expressionRewritingBehaviors');
require('./extenderBehaviors');
require('./mappingHelperBehaviors');
require('./observableArrayBehaviors');
require('./observableBehaviors');
require('./subscribableBehaviors');

// get reference to jasmine runtime
var env = jasmine.jasmine.getEnv();

// create reporter to return results
function failureFilter(item) {
return !item.passed();
}
env.addReporter({
reportRunnerResults:function (runner) {
var results = runner.results();
runner.suites().map(function (suite) {
// hack around suite results not having a description
var suiteResults = suite.results();
suiteResults.description = suite.description;
return suiteResults;
}).filter(failureFilter).forEach(function (suite) {
console.error(suite.description);
suite.getItems().filter(failureFilter).forEach(function (spec) {
console.error('\t' + spec.description);
spec.getItems().filter(failureFilter).forEach(function (expectation) {
console.error('\t\t' + expectation.message);
});
});
});
console.log("Total:" + results.totalCount + " Passed:" + results.passedCount + " Failed:" + results.failedCount);
process.exit(results.failedCount);
}
});

// good to go
env.execute();
3 changes: 3 additions & 0 deletions spec/runner.phantom.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ function waitFor(testFx, onReady, timeOutMillis) {

var page = require('webpage').create();

console.log("Running Knockout tests in Phantom.js");


// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
page.onConsoleMessage = function(msg) {
console.log(msg);
Expand Down
2 changes: 1 addition & 1 deletion src/subscribables/dependentObservable.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
// plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
if (disposeWhenNodeIsRemoved && isActive()) {
dispose = function() {
ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
disposeAllSubscriptionsToDependencies();
};
ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
Expand Down
10 changes: 5 additions & 5 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ko.utils = (function () {

// Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
var knownEvents = {}, knownEventTypesByEventName = {};
var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
var keyEventTypeName = (navigator && /Firefox\/2/i.test(navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents';
knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
objectForEach(knownEvents, function(eventType, knownEventsForType) {
Expand All @@ -26,7 +26,7 @@ ko.utils = (function () {
// 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.
var ieVersion = (function() {
var ieVersion = document && (function() {
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
Expand Down Expand Up @@ -411,16 +411,16 @@ ko.utils = (function () {
if (typeof jsonString == "string") {
jsonString = ko.utils.stringTrim(jsonString);
if (jsonString) {
if (window.JSON && window.JSON.parse) // Use native parsing where available
return window.JSON.parse(jsonString);
if (JSON && JSON.parse) // Use native parsing where available
return JSON.parse(jsonString);
return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
}
}
return null;
},

stringifyJson: function (data, replacer, space) { // replacer and space are optional
if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
if (!JSON || !JSON.stringify)
throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space);
},
Expand Down
Loading

0 comments on commit 5af83af

Please sign in to comment.